Merge history from osmo-gsm-manuals.git

Change-Id: I175d694bea8f0be4edb2be75de780dee82469849
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cba941d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+*.o
+*.lo
+*.la
+*.db
+*.pyc
+.*.sw?
+.version
+.tarball-version
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.guess
+config.log
+config.status
+config.sub
+configure
+depcomp
+install-sh
+libtool
+ltmain.sh
+m4
+*.m4
+missing
+.deps
+
+*.pc
+.libs
+
+src/db_test
+src/db_bootstrap.h
+src/osmo-hlr
+src/osmo-hlr-db-tool
+src/osmo-euse-demo
+src/gsupclient/gsup-test-client
+
+tests/atconfig
+tests/testsuite
+tests/testsuite.log
+
+tests/auc/auc_3g_test
+tests/auc/auc_ts_55_205_test_sets.c
+tests/auc/auc_ts_55_205_test_sets
+tests/auc/auc_test
+tests/gsup_server/gsup_server_test
+tests/gsup/gsup_test
+tests/db/db_test
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..afddc7b
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,3 @@
+[gerrit]
+host=gerrit.osmocom.org
+project=osmo-hlr
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..210e499
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,28 @@
+AUTOMAKE_OPTIONS = foreign dist-bzip2
+
+SUBDIRS = \
+	doc \
+	src \
+	include \
+	sql \
+	contrib \
+	tests \
+	$(NULL)
+
+EXTRA_DIST = \
+	.version \
+	$(NULL)
+
+DISTCHECK_CONFIGURE_FLAGS = \
+	--with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir)
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libosmo-gsup-client.pc
+
+@RELMAKE@
+
+BUILT_SOURCES = $(top_srcdir)/.version
+$(top_srcdir)/.version:
+	echo $(VERSION) > $@-t && mv $@-t $@
+dist-hook:
+	echo $(VERSION) > $(distdir)/.tarball-version
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..99f7829
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,131 @@
+AC_INIT([osmo-hlr],
+	m4_esyscmd([./git-version-gen .tarball-version]),
+	[openbsc@lists.osmocom.org])
+
+dnl *This* is the root dir, even if an install-sh exists in ../ or ../../
+AC_CONFIG_AUX_DIR([.])
+
+dnl libtool init
+LT_INIT
+
+AM_INIT_AUTOMAKE([foreign dist-bzip2 no-dist-gzip 1.9])
+
+AC_CONFIG_TESTDIR(tests)
+
+dnl kernel style compile messages
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+
+dnl include release helper
+RELMAKE='-include osmo-release.mk'
+AC_SUBST([RELMAKE])
+
+dnl checks for programs
+AC_PROG_MAKE_SET
+AC_PROG_MKDIR_P
+AC_PROG_CC
+AC_PROG_INSTALL
+
+dnl check for pkg-config (explained in detail in libosmocore/configure.ac)
+AC_PATH_PROG(PKG_CONFIG_INSTALLED, pkg-config, no)
+if test "x$PKG_CONFIG_INSTALLED" = "xno"; then
+        AC_MSG_WARN([You need to install pkg-config])
+fi
+PKG_PROG_PKG_CONFIG([0.20])
+
+PKG_CHECK_MODULES(TALLOC, [talloc >= 2.0.1])
+
+PKG_CHECK_MODULES(LIBOSMOCORE, libosmocore >= 0.11.0)
+PKG_CHECK_MODULES(LIBOSMOGSM, libosmogsm >= 0.11.0)
+PKG_CHECK_MODULES(LIBOSMOVTY, libosmovty >= 0.11.0)
+PKG_CHECK_MODULES(LIBOSMOCTRL, libosmoctrl >= 0.11.0)
+PKG_CHECK_MODULES(LIBOSMOABIS, libosmoabis >= 0.5.0)
+
+PKG_CHECK_MODULES(SQLITE3, sqlite3)
+
+AC_CONFIG_MACRO_DIR([m4])
+
+dnl checks for header files
+AC_HEADER_STDC
+
+AC_ARG_ENABLE(sanitize,
+	[AS_HELP_STRING(
+		[--enable-sanitize],
+		[Compile with address sanitizer enabled],
+	)],
+	[sanitize=$enableval], [sanitize="no"])
+if test x"$sanitize" = x"yes"
+then
+	CFLAGS="$CFLAGS -fsanitize=address -fsanitize=undefined"
+	CPPFLAGS="$CPPFLAGS -fsanitize=address -fsanitize=undefined"
+fi
+
+AC_ARG_ENABLE(werror,
+	[AS_HELP_STRING(
+		[--enable-werror],
+		[Turn all compiler warnings into errors, with exceptions:
+		 a) deprecation (allow upstream to mark deprecation without breaking builds);
+		 b) "#warning" pragmas (allow to remind ourselves of errors without breaking builds)
+		]
+	)],
+	[werror=$enableval], [werror="no"])
+if test x"$werror" = x"yes"
+then
+	WERROR_FLAGS="-Werror"
+	WERROR_FLAGS+=" -Wno-error=deprecated -Wno-error=deprecated-declarations"
+	WERROR_FLAGS+=" -Wno-error=cpp" # "#warning"
+	CFLAGS="$CFLAGS $WERROR_FLAGS"
+	CPPFLAGS="$CPPFLAGS $WERROR_FLAGS"
+fi
+
+AC_ARG_ENABLE([external_tests],
+		AC_HELP_STRING([--enable-external-tests],
+				[Include the VTY/CTRL tests in make check [default=no]]),
+		[enable_ext_tests="$enableval"],[enable_ext_tests="no"])
+if test "x$enable_ext_tests" = "xyes" ; then
+	AM_PATH_PYTHON
+	AC_CHECK_PROG(OSMOTESTEXT_CHECK,osmotestvty.py,yes)
+	 if test "x$OSMOTESTEXT_CHECK" != "xyes" ; then
+		AC_MSG_ERROR([Please install git://osmocom.org/python/osmo-python-tests to run the VTY/CTRL tests.])
+	fi
+fi
+AC_MSG_CHECKING([whether to enable VTY/CTRL tests])
+AC_MSG_RESULT([$enable_ext_tests])
+AM_CONDITIONAL(ENABLE_EXT_TESTS, test "x$enable_ext_tests" = "xyes")
+
+# https://www.freedesktop.org/software/systemd/man/daemon.html
+AC_ARG_WITH([systemdsystemunitdir],
+     [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files])],,
+     [with_systemdsystemunitdir=auto])
+AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], [
+     def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)
+
+     AS_IF([test "x$def_systemdsystemunitdir" = "x"],
+   [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"],
+    [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])])
+    with_systemdsystemunitdir=no],
+   [with_systemdsystemunitdir="$def_systemdsystemunitdir"])])
+AS_IF([test "x$with_systemdsystemunitdir" != "xno"],
+      [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])])
+AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"])
+
+AC_MSG_RESULT([CFLAGS="$CFLAGS"])
+AC_MSG_RESULT([CPPFLAGS="$CPPFLAGS"])
+
+AC_OUTPUT(
+	Makefile
+	doc/Makefile
+	doc/examples/Makefile
+	src/Makefile
+	src/gsupclient/Makefile
+	include/Makefile
+	libosmo-gsup-client.pc
+	sql/Makefile
+	contrib/Makefile
+	contrib/systemd/Makefile
+	tests/Makefile
+	tests/auc/Makefile
+	tests/auc/gen_ts_55_205_test_sets/Makefile
+	tests/gsup_server/Makefile
+	tests/gsup/Makefile
+	tests/db/Makefile
+	)
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
new file mode 100644
index 0000000..3439c97
--- /dev/null
+++ b/contrib/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = systemd
diff --git a/contrib/jenkins.sh b/contrib/jenkins.sh
new file mode 100755
index 0000000..8dc0162
--- /dev/null
+++ b/contrib/jenkins.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+# jenkins build helper script for osmo-hlr.  This is how we build on jenkins.osmocom.org
+
+if ! [ -x "$(command -v osmo-build-dep.sh)" ]; then
+	echo "Error: We need to have scripts/osmo-deps.sh from http://git.osmocom.org/osmo-ci/ in PATH !"
+	exit 2
+fi
+
+
+set -ex
+
+base="$PWD"
+deps="$base/deps"
+inst="$deps/install"
+export deps inst
+
+osmo-clean-workspace.sh
+
+mkdir "$deps" || true
+
+verify_value_string_arrays_are_terminated.py $(find . -name "*.[hc]")
+
+export PKG_CONFIG_PATH="$inst/lib/pkgconfig:$PKG_CONFIG_PATH"
+export LD_LIBRARY_PATH="$inst/lib"
+
+osmo-build-dep.sh libosmocore "" ac_cv_path_DOXYGEN=false
+osmo-build-dep.sh libosmo-abis
+
+set +x
+echo
+echo
+echo
+echo " =============================== osmo-hlr ==============================="
+echo
+set -x
+
+cd "$base"
+autoreconf --install --force
+./configure --enable-sanitize --enable-external-tests --enable-werror
+$MAKE $PARALLEL_MAKE
+$MAKE check || cat-testlogs.sh
+$MAKE distcheck || cat-testlogs.sh
+
+osmo-clean-workspace.sh
diff --git a/contrib/systemd/Makefile.am b/contrib/systemd/Makefile.am
new file mode 100644
index 0000000..520e720
--- /dev/null
+++ b/contrib/systemd/Makefile.am
@@ -0,0 +1,5 @@
+if HAVE_SYSTEMD
+EXTRA_DIST = osmo-hlr.service
+systemdsystemunit_DATA = \
+  osmo-hlr.service
+endif
diff --git a/contrib/systemd/osmo-hlr.service b/contrib/systemd/osmo-hlr.service
new file mode 100644
index 0000000..64e369d
--- /dev/null
+++ b/contrib/systemd/osmo-hlr.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Osmocom Home Location Register (OsmoHLR)
+
+[Service]
+Type=simple
+Restart=always
+ExecStart=/usr/bin/osmo-hlr -c /etc/osmocom/osmo-hlr.cfg -l /var/lib/osmocom/hlr.db
+RestartSec=2
+
+[Install]
+WantedBy=multi-user.target
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..3763f55
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,207 @@
+osmo-hlr (0.2.1) unstable; urgency=medium
+
+  [ Neels Hofmeyr ]
+  * fix luop crash: use buffer for APN that remains valid
+  * add gsup_test to catch OS#3231
+  * add error handling to osmo_gsup_configure_wildcard_apn()
+
+ -- Pau Espin Pedrol <pespin@sysmocom.de>  Fri, 04 May 2018 18:41:35 +0200
+
+osmo-hlr (0.2.0) unstable; urgency=medium
+
+  [ Neels Hofmeyr ]
+  * vty: skip installing cmds now always installed by default
+  * hlr_db_tool: fix error log strerror invocation
+  * cosmetic: add comment on ignored return value
+  * db-tool: add command 'create'
+  * db-tool: cosmetic: tweak printf output
+  * db-tool: error-exit on too many arguments
+  * add --enable-sanitize config option
+  * db_test: don't verify SQLite issued error messages, they might change
+  * cosmetic: rx_send_auth_info(): decide error cause with switch()
+  * return GMM_CAUSE_IMSI_UNKNOWN if there is no auth data
+  * db_get_auth_data / db_get_auc: clarify return values
+  * osmo-hlr: log details for unknown IMSI / no auth data / db error
+  * db_test: also test db_get_auc() return values
+  * fix test_subscriber_errors.ctrl after libosmocore change
+  * fix debug log: put 'deriving 2G from 3G' in proper place
+  * ctrl test: fix: adjust expectations after stricter ctrl parsing
+  * fix build: db_test: missing LIBOSMOABIS_CFLAGS and _LIBS
+  * configure: add --enable-werror
+  * jenkins.sh: use --enable-werror configure flag, not CFLAGS
+
+  [ Harald Welte ]
+  * hlr.c: Avoid overflow of lu_operation.subscr.imsi
+  * Fix expected test output after new 'logging print file 1' vty command
+  * osmo-hlr: Add talloc context introspection via VTY
+  * vty: Don't print error if removing auth data while none present
+  * Fix responses to PURGE MS
+
+  [ Alexander Couzens ]
+  * debian: include systemd service osmo-hlr.service
+  * doc: install example .cfg files to $(docdir)/examples/
+  * debian: install osmo-hlr.cfg to /etc/osmocom
+
+  [ Max ]
+  * Remove unused check
+  * Remove unused ipa.py
+  * Enable sanitize for CI tests
+
+  [ Pau Espin Pedrol ]
+  * luop.c: Transform FIXME from warning to pragma message
+  * contrib:jenkins.sh: Enable Werror
+  * use osmo_init_logging2
+  * Remove unused src/db_test.c
+
+  [ Alexander Huemer ]
+  * Add missing build products in .gitignore
+
+  [ Stefan Sperling ]
+  * more robust usage of osmo_timer API for osmo-hlr luop timer
+  * notify GSUP clients when HLR subscriber information changes
+  * rewrite subscriber_update_notify() without calls into luop
+  * don't forget to mark luop as a packet switched connection
+
+ -- Pau Espin Pedrol <pespin@sysmocom.de>  Thu, 03 May 2018 16:27:13 +0200
+
+osmo-hlr (0.1.0) unstable; urgency=medium
+
+  [ Neels Hofmeyr ]
+  * build with autoconf/automake, add jenkins.sh script
+  * fix build on FreeBSD: eliminate implicitly declared functions
+  * fix various compiler warnings
+  * fix DLGSUP logging cat after change in libosmocore
+  * build: recoin db_test as non-installable program
+  * build: actually make sqlite mandatory
+  * bump required libosmocore version to 0.9.5
+  * gsup: send subscriber MSISDN
+  * debug log: log computed vector kinds
+  * log: move a log from info to debug level
+  * hlr.sql: typo in comment
+  * auc.c: typo in comment
+  * main: add and use root talloc ctx
+  * main: add option parsing with db file and default options
+  * main: add VTY and '-c config-file' option
+  * sql: fix 3g_auc's column K data type
+  * cosmetic: sql: indicate VARCHAR size of key columns as 32
+  * sql: auc_3g: set sqn NOT NULL DEFAULT 0
+  * comment: sql: describe auc_2g and auc_3g columns
+  * Add test suite skeleton with empty test (auc_3g_test)
+  * tests: auc_3g_test: implement vector generation test
+  * auth: verify test sets from 3GPP TS 55.205
+  * sql: add unique constraints to IMSI and MSISDN
+  * UMTS AKA resync: fix argument ordering
+  * auc_3g_test: add AUTS resync test
+  * auc_gen_vectors(): ensure sane arguments, test
+  * auc_3g_test: allow to inc fake rand bytes upon rand request
+  * auc_3g_test: add AUTS test with N vectors, to show bug
+  * cosmetic: refactor auc_compute_vectors(), add debug log
+  * auc_compute_vectors(): fix AUTS resync for multiple vectors
+  * cosmetic: auc_3g_test: improve test debugging tools
+  * cosmetic: rename auc_3g_test.c to auc_test.c
+  * fix: properly handle error rc by osmo_gsup_conn_ccm_get()
+  * auc tests: adjust cosmetically to prepare for SQN changes
+  * auc tests: fix after SQN scheme changes from libosmocore
+  * fix debug log: adjust to new SQN increment scheme
+  * UMTS AKA: implement SQN increment according to SEQ and IND
+  * debug log: output ind slot, previous sqn, and sqn db update
+  * jenkins: add value_string termination check
+  * fix db_subscr_ps error handling
+  * add config example (mostly empty)
+  * install hlr.sql in prefix/doc/osmo-hlr/
+  * use OSMO_GSUP_PORT == 4222 instead of hardcoded 2222
+  * add basic CTRL interface tests
+  * add CTRL tests for enable-/disable-/status-ps
+  * cosmetic: prepend DB_STMT_ to enum stmt_idx entries
+  * cosmetic: rename db_subscr_get() to db_subscr_get_by_imsi()
+  * cosmetic: refactor db_bind_imsi() as db_bind_text()
+  * cosmetic: multi-line DB_STMT_AUC_BY_IMSI
+  * cosmetic: log IMSI='<imsi>', log "no such subscriber"
+  * cosmetic: log: "SQLite" with capital L
+  * cosmetic: db_hlr: SL3_TXT: clarify indenting
+  * ctrl_test_runner.py: use proper constant as test db path
+  * gitignore: tests/package.m4
+  * cosmetic: don't log about missing SQLite log cb
+  * add db_bind_int() and db_bind_int64()
+  * add db_subscr_create(), db_subscr_delete(), db_subscr_update_msisdn_by_imsi()
+  * add initial db_test: creating and deleting subscribers
+  * less noise: simplify db_remove_reset()
+  * db: use int64_t as subscriber id
+  * add db_subscr_get_by_msisdn() and db_subscr_get_by_id()
+  * refactor db_subscr_ps() to db_subscr_nam()
+  * refactor db_subscr_lu()
+  * refactor db_subscr_purge
+  * add db_subscr_update_aud_by_id(), complete db_subscr_delete_by_id()
+  * refactor db_get_auth_data return val
+  * code undup: use db_remove_reset() in db_auc.c
+  * fix db_update_sqn(): reset stmt in all error cases
+  * code undup: use db_bind_text() in db_get_auth_data()
+  * debian: 'make check' needs sqlite3, add to Build-Depends
+  * fix db_subscr_get_by_*(): clear output data; test in db_test.c
+  * implement subscriber vty interface, tests
+  * add test_nodes.vty
+  * replace ctrl_test_runner.py with transcript test_subscriber.ctrl
+  * add lu_op_free(), use in luop.c
+  * luop: fix mem leak upon error in lu_op_alloc_conn()
+  * fix mem leak in handle_cmd_ps(): free luop
+  * api doc: say that lu_op_tx_del_subscr_data() doesn't free
+  * add hlr_subsrc_nam to put GSUP client notification in proper API
+  * vty: fix output of empty IMSI
+  * db api: fix/add API docs
+  * cosmetic: tweak params of hlr_controlif_setup()
+  * ctrl: completely replace all CTRL commands
+  * test_subscriber.ctrl: test against octal/hex interpretation of id
+  * jenkins: use osmo-clean-workspace.sh before and after build
+  * tests/Makefile: use test db var instead of repeating the path
+  * db_test: fix *FLAGS
+  * automatically create db tables on osmo-hlr invocation
+  * cosmetic: sql/hlr.sql: move comments
+  * cosmetic: rename SL3_TXT macro, use osmo_strlcpy()
+  * fix default logging levels to NOTICE, not DEBUG
+  * add osmo-hlr-db-tool, program to migrate from osmo-nitb db
+
+  [ Max ]
+  * Add gerrit settings
+  * Add hardcoded APN
+  * Log error cause as a string
+  * Move GSUP msg init into separate function
+  * Use strings for GSUP message type
+  * Move lu_operation into separate file
+  * db: move duplicated code into helper functions
+  * Fix compiler's warning about printf security
+  * Add routines to update nam_ps
+  * Add global HLR struct
+  * Make subscr parameter to db_subscr_get() optional
+  * Add CTRL interface
+  * CTRL: add enable/disable packet service cmds
+  * Add .deb packaging
+  * deb: fix OBS build
+  * debian: remove obsolete dependency
+  * Attempt to fix .deb package
+  * deb: use python in shebang
+  * Another attempt at fixing .deb
+  * Use release helper from libosmocore
+  * Use value string check from osmo-ci
+
+  [ Daniel Willmann ]
+  * Add systemd service file
+  * hlr_data.sql: Insert ki and opc instead of op to example data
+  * tests/auc: Don't require python3
+
+  [ Pau Espin Pedrol ]
+  * VTY: Add hlr node and bind ip field
+  * debian: remove unneeded dependency libdbd-sqlite3
+
+  [ Harald Welte ]
+  * jenkins.sh: Proper error message if local environment isn't set up
+
+  [ Alexander Couzens ]
+  * debian/rules: show testsuite.log when tests are failing
+
+ -- Harald Welte <laforge@gnumonks.org>  Sat, 28 Oct 2017 20:37:33 +0200
+
+osmo-hlr (0.0.1) UNRELEASED; urgency=low
+
+     * Initial release (Closes: OS#1948)
+
+ -- Max Suraev <msuraev@sysmocom.de>  Mon, 13 Mar 2017 16:26:41 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..23c39ce
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,59 @@
+Source: osmo-hlr
+Section: net
+Priority: optional
+Maintainer: Max Suraev <msuraev@sysmocom.de>
+Build-Depends: debhelper (>= 9),
+               pkg-config,
+               dh-autoreconf,
+               dh-systemd (>= 1.5),
+               autotools-dev,
+               python-minimal,
+               libosmocore-dev,
+               libosmo-abis-dev,
+               libosmo-netif-dev,
+               libsqlite3-dev,
+               sqlite3
+Standards-Version: 3.9.6
+Vcs-Browser: http://cgit.osmocom.org/osmo-hlr
+Vcs-Git: git://git.osmocom.org/osmo-hlr
+Homepage: https://projects.osmocom.org/projects/osmo-hlr
+
+Package: osmo-hlr
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}, libdbd-sqlite3
+Description: Osmocom Home Location Register
+ OsmoHLR is a Osmocom implementation of HLR (Home Location Registrar) which works over GSUP
+ protocol. The subscribers are store in sqlite DB. It supports both 2G and 3G authentication.
+
+Package: osmo-hlr-dbg
+Architecture: any
+Section: debug
+Priority: extra
+Depends: osmo-hlr (= ${binary:Version}), ${misc:Depends}
+Description: Debug symbols for the osmo-hlr
+ Make debugging possible
+
+Package: libosmo-gsup-client0
+Section: libs
+Architecture: any
+Multi-Arch: same
+Depends: ${shlibs:Depends},
+	 ${misc:Depends}
+Pre-Depends: ${misc:Pre-Depends}
+Description: Osmocom GSUP (General Subscriber Update Protocol) client library
+  This is a shared library that can be used to implement client programs for
+  the GSUP protocol.  The typical GSUP server is OsmoHLR, with OsmoMSC, OsmoSGSN
+  and External USSD Entities (EUSEs) using this library to implement clients.
+
+Package: libosmo-gsup-client-dev
+Architecture: any
+Multi-Arch: same
+Depends: ${misc:Depends},
+	 libosmo-gsup-client0 (= ${binary:Version}),
+	 libosmocore-dev
+Description: Development headers of Osmocom GSUP client library
+  This is a shared library that can be used to implement client programs for
+  the GSUP protocol.  The typical GSUP server is OsmoHLR, with OsmoMSC, OsmoSGSN
+  and External USSD Entities (EUSEs) using this library to implement clients.
+  .
+  This package contains the development headers.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..c0e2b45
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,21 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: OsmoHLR
+Source: http://cgit.osmocom.org/osmo-hlr/
+
+Files: *
+Copyright: 2016-2017 Sysmocom s. f. m. c. GmbH <info@sysmocom.de>
+License: AGPL-3+
+
+License: AGPL-3+
+ 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/>.
diff --git a/debian/libosmo-gsup-client-dev.install b/debian/libosmo-gsup-client-dev.install
new file mode 100644
index 0000000..8a75973
--- /dev/null
+++ b/debian/libosmo-gsup-client-dev.install
@@ -0,0 +1,5 @@
+usr/include/osmocom/gsupclient
+usr/lib/*/libosmo-gsup-client*.a
+usr/lib/*/libosmo-gsup-client*.so
+usr/lib/*/libosmo-gsup-client*.la
+usr/lib/*/pkgconfig/libosmo-gsup-client.pc
diff --git a/debian/libosmo-gsup-client0.install b/debian/libosmo-gsup-client0.install
new file mode 100644
index 0000000..78cd89f
--- /dev/null
+++ b/debian/libosmo-gsup-client0.install
@@ -0,0 +1 @@
+usr/lib/*/libosmo-gsup-client*.so.*
diff --git a/debian/osmo-hlr.install b/debian/osmo-hlr.install
new file mode 100644
index 0000000..1b2cb56
--- /dev/null
+++ b/debian/osmo-hlr.install
@@ -0,0 +1,8 @@
+/etc/osmocom/osmo-hlr.cfg
+/lib/systemd/system/osmo-hlr.service
+/usr/bin/osmo-hlr
+/usr/bin/osmo-hlr-db-tool
+/usr/share/doc/osmo-hlr/sql/hlr.sql
+/usr/share/doc/osmo-hlr/sql/hlr_data.sql
+/usr/share/doc/osmo-hlr/examples/osmo-hlr.cfg
+/var/lib/osmocom
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..9e97c6c
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,20 @@
+#!/usr/bin/make -f
+
+#export DH_VERBOSE=1
+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+
+%:
+	dh $@ --with autoreconf
+
+override_dh_shlibdeps:
+	dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info
+
+override_dh_strip:
+	dh_strip --dbg-package=osmo-hlr-dbg
+
+# Print test results in case of a failure
+override_dh_auto_test:
+	dh_auto_test || (find . -name testsuite.log -exec cat {} \; ; false)
+
+override_dh_auto_configure:
+	dh_auto_configure -- --with-systemdsystemunitdir=/lib/systemd/system
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..9f67427
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
\ No newline at end of file
diff --git a/doc/Makefile.am b/doc/Makefile.am
new file mode 100644
index 0000000..aee2d7b
--- /dev/null
+++ b/doc/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = examples
diff --git a/doc/examples/Makefile.am b/doc/examples/Makefile.am
new file mode 100644
index 0000000..fa8ab9b
--- /dev/null
+++ b/doc/examples/Makefile.am
@@ -0,0 +1,27 @@
+osmoconfdir = $(sysconfdir)/osmocom
+osmoconf_DATA = osmo-hlr.cfg
+
+EXTRA_DIST = osmo-hlr.cfg
+
+CFG_FILES = find $(srcdir) -name '*.cfg*' | sed -e 's,^$(srcdir),,'
+
+dist-hook:
+	for f in $$($(CFG_FILES)); do \
+		j="$(distdir)/$$f" && \
+		mkdir -p "$$(dirname $$j)" && \
+		$(INSTALL_DATA) $(srcdir)/$$f $$j; \
+	done
+
+install-data-hook:
+	for f in $$($(CFG_FILES)); do \
+		j="$(DESTDIR)$(docdir)/examples/$$f" && \
+		mkdir -p "$$(dirname $$j)" && \
+		$(INSTALL_DATA) $(srcdir)/$$f $$j; \
+	done
+
+uninstall-hook:
+	@$(PRE_UNINSTALL)
+	for f in $$($(CFG_FILES)); do \
+		j="$(DESTDIR)$(docdir)/examples/$$f" && \
+		$(RM) $$j; \
+	done
diff --git a/doc/examples/osmo-hlr.cfg b/doc/examples/osmo-hlr.cfg
new file mode 100644
index 0000000..a685858
--- /dev/null
+++ b/doc/examples/osmo-hlr.cfg
@@ -0,0 +1,26 @@
+!
+! OsmoHLR example configuration
+!
+log stderr
+ logging filter all 1
+ logging color 1
+ logging print category 1
+ logging print category-hex 0
+ logging print level 1
+ logging print file basename last
+ logging print extended-timestamp 1
+ logging level main notice
+ logging level db notice
+ logging level auc notice
+ logging level ss info
+ logging level linp error
+!
+line vty
+ bind 127.0.0.1
+ctrl
+ bind 127.0.0.1
+hlr
+ gsup
+  bind ip 127.0.0.1
+ ussd route prefix *#100# internal own-msisdn
+ ussd route prefix *#101# internal own-imsi
diff --git a/git-version-gen b/git-version-gen
new file mode 100755
index 0000000..42cf3d2
--- /dev/null
+++ b/git-version-gen
@@ -0,0 +1,151 @@
+#!/bin/sh
+# Print a version string.
+scriptversion=2010-01-28.01
+
+# Copyright (C) 2007-2010 Free Software Foundation, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/.
+# It may be run two ways:
+# - from a git repository in which the "git describe" command below
+#   produces useful output (thus requiring at least one signed tag)
+# - from a non-git-repo directory containing a .tarball-version file, which
+#   presumes this script is invoked like "./git-version-gen .tarball-version".
+
+# In order to use intra-version strings in your project, you will need two
+# separate generated version string files:
+#
+# .tarball-version - present only in a distribution tarball, and not in
+#   a checked-out repository.  Created with contents that were learned at
+#   the last time autoconf was run, and used by git-version-gen.  Must not
+#   be present in either $(srcdir) or $(builddir) for git-version-gen to
+#   give accurate answers during normal development with a checked out tree,
+#   but must be present in a tarball when there is no version control system.
+#   Therefore, it cannot be used in any dependencies.  GNUmakefile has
+#   hooks to force a reconfigure at distribution time to get the value
+#   correct, without penalizing normal development with extra reconfigures.
+#
+# .version - present in a checked-out repository and in a distribution
+#   tarball.  Usable in dependencies, particularly for files that don't
+#   want to depend on config.h but do want to track version changes.
+#   Delete this file prior to any autoconf run where you want to rebuild
+#   files to pick up a version string change; and leave it stale to
+#   minimize rebuild time after unrelated changes to configure sources.
+#
+# It is probably wise to add these two files to .gitignore, so that you
+# don't accidentally commit either generated file.
+#
+# Use the following line in your configure.ac, so that $(VERSION) will
+# automatically be up-to-date each time configure is run (and note that
+# since configure.ac no longer includes a version string, Makefile rules
+# should not depend on configure.ac for version updates).
+#
+# AC_INIT([GNU project],
+#         m4_esyscmd([build-aux/git-version-gen .tarball-version]),
+#         [bug-project@example])
+#
+# Then use the following lines in your Makefile.am, so that .version
+# will be present for dependencies, and so that .tarball-version will
+# exist in distribution tarballs.
+#
+# BUILT_SOURCES = $(top_srcdir)/.version
+# $(top_srcdir)/.version:
+#	echo $(VERSION) > $@-t && mv $@-t $@
+# dist-hook:
+#	echo $(VERSION) > $(distdir)/.tarball-version
+
+case $# in
+    1) ;;
+    *) echo 1>&2 "Usage: $0 \$srcdir/.tarball-version"; exit 1;;
+esac
+
+tarball_version_file=$1
+nl='
+'
+
+# First see if there is a tarball-only version file.
+# then try "git describe", then default.
+if test -f $tarball_version_file
+then
+    v=`cat $tarball_version_file` || exit 1
+    case $v in
+	*$nl*) v= ;; # reject multi-line output
+	[0-9]*) ;;
+	*) v= ;;
+    esac
+    test -z "$v" \
+	&& echo "$0: WARNING: $tarball_version_file seems to be damaged" 1>&2
+fi
+
+if test -n "$v"
+then
+    : # use $v
+elif
+       v=`git describe --abbrev=4 --match='v*' HEAD 2>/dev/null \
+	  || git describe --abbrev=4 HEAD 2>/dev/null` \
+    && case $v in
+	 [0-9]*) ;;
+	 v[0-9]*) ;;
+	 *) (exit 1) ;;
+       esac
+then
+    # Is this a new git that lists number of commits since the last
+    # tag or the previous older version that did not?
+    #   Newer: v6.10-77-g0f8faeb
+    #   Older: v6.10-g0f8faeb
+    case $v in
+	*-*-*) : git describe is okay three part flavor ;;
+	*-*)
+	    : git describe is older two part flavor
+	    # Recreate the number of commits and rewrite such that the
+	    # result is the same as if we were using the newer version
+	    # of git describe.
+	    vtag=`echo "$v" | sed 's/-.*//'`
+	    numcommits=`git rev-list "$vtag"..HEAD | wc -l`
+	    v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`;
+	    ;;
+    esac
+
+    # Change the first '-' to a '.', so version-comparing tools work properly.
+    # Remove the "g" in git describe's output string, to save a byte.
+    v=`echo "$v" | sed 's/-/./;s/\(.*\)-g/\1-/'`;
+else
+    v=UNKNOWN
+fi
+
+v=`echo "$v" |sed 's/^v//'`
+
+# Don't declare a version "dirty" merely because a time stamp has changed.
+git status > /dev/null 2>&1
+
+dirty=`sh -c 'git diff-index --name-only HEAD' 2>/dev/null` || dirty=
+case "$dirty" in
+    '') ;;
+    *) # Append the suffix only if there isn't one already.
+	case $v in
+	  *-dirty) ;;
+	  *) v="$v-dirty" ;;
+	esac ;;
+esac
+
+# Omit the trailing newline, so that m4_esyscmd can use the result directly.
+echo "$v" | tr -d '\012'
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-end: "$"
+# End:
diff --git a/include/Makefile.am b/include/Makefile.am
new file mode 100644
index 0000000..dac7011
--- /dev/null
+++ b/include/Makefile.am
@@ -0,0 +1,2 @@
+nobase_include_HEADERS = osmocom/gsupclient/gsup_client.h
+
diff --git a/include/osmocom/gsupclient/gsup_client.h b/include/osmocom/gsupclient/gsup_client.h
new file mode 100644
index 0000000..981751b
--- /dev/null
+++ b/include/osmocom/gsupclient/gsup_client.h
@@ -0,0 +1,65 @@
+/* GPRS Subscriber Update Protocol client */
+
+/* (C) 2014 by Sysmocom s.f.m.c. GmbH
+ * All Rights Reserved
+ *
+ * Author: Jacob Erlbeck
+ *
+ * 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/>.
+ *
+ */
+#pragma once
+
+#include <osmocom/core/timer.h>
+#include <osmocom/gsm/oap_client.h>
+
+/* a loss of GSUP between MSC and HLR is considered quite serious, let's try to recover as quickly as
+ * possible.  Even one new connection attempt per second should be quite acceptable until the link is
+ * re-established */
+#define OSMO_GSUP_CLIENT_RECONNECT_INTERVAL 1
+#define OSMO_GSUP_CLIENT_PING_INTERVAL 20
+
+struct msgb;
+struct ipa_client_conn;
+struct osmo_gsup_client;
+
+/* Expects message in msg->l2h */
+typedef int (*osmo_gsup_client_read_cb_t)(struct osmo_gsup_client *gsupc, struct msgb *msg);
+
+struct osmo_gsup_client {
+	const char *unit_name;
+
+	struct ipa_client_conn *link;
+	osmo_gsup_client_read_cb_t read_cb;
+	void *data;
+
+	struct osmo_oap_client_state oap_state;
+
+	struct osmo_timer_list ping_timer;
+	struct osmo_timer_list connect_timer;
+	int is_connected;
+	int got_ipa_pong;
+};
+
+struct osmo_gsup_client *osmo_gsup_client_create(void *talloc_ctx,
+						 const char *unit_name,
+						 const char *ip_addr,
+						 unsigned int tcp_port,
+						 osmo_gsup_client_read_cb_t read_cb,
+						 struct osmo_oap_client_config *oapc_config);
+
+void osmo_gsup_client_destroy(struct osmo_gsup_client *gsupc);
+int osmo_gsup_client_send(struct osmo_gsup_client *gsupc, struct msgb *msg);
+struct msgb *osmo_gsup_client_msgb_alloc(void);
+
diff --git a/libosmo-gsup-client.pc.in b/libosmo-gsup-client.pc.in
new file mode 100644
index 0000000..8bbae08
--- /dev/null
+++ b/libosmo-gsup-client.pc.in
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: Osmocom GSUP Client Library
+Description: C Utility Library
+Version: @VERSION@
+Libs: -L${libdir} -losmo-gsup-client
+Cflags: -I${includedir}/
+
diff --git a/sql/Makefile.am b/sql/Makefile.am
new file mode 100644
index 0000000..e84943b
--- /dev/null
+++ b/sql/Makefile.am
@@ -0,0 +1,14 @@
+EXTRA_DIST = \
+	hlr_data.sql \
+	hlr.sql \
+	$(NULL)
+
+sqldir = $(docdir)/sql
+sql_DATA = $(srcdir)/hlr.sql $(srcdir)/hlr_data.sql
+
+
+install-data-local:
+	$(MKDIR_P) $(DESTDIR)$(localstatedir)/lib/osmocom
+
+uninstall-hook:
+	rm -rf $(DESTDIR)$(localstatedir)/lib/osmocom
diff --git a/sql/hlr.sql b/sql/hlr.sql
new file mode 100644
index 0000000..80eb3e5
--- /dev/null
+++ b/sql/hlr.sql
@@ -0,0 +1,69 @@
+CREATE TABLE IF NOT EXISTS subscriber (
+-- OsmoHLR's DB scheme is modelled roughly after TS 23.008 version 13.3.0
+	id		INTEGER PRIMARY KEY,
+	-- Chapter 2.1.1.1
+	imsi		VARCHAR(15) UNIQUE NOT NULL,
+	-- Chapter 2.1.2
+	msisdn		VARCHAR(15) UNIQUE,
+	-- Chapter 2.2.3: Most recent / current IMEI
+	imeisv		VARCHAR,
+	-- Chapter 2.4.5
+	vlr_number	VARCHAR(15),
+	-- Chapter 2.4.6
+	hlr_number	VARCHAR(15),
+	-- Chapter 2.4.8.1
+	sgsn_number	VARCHAR(15),
+	-- Chapter 2.13.10
+	sgsn_address	VARCHAR,
+	-- Chapter 2.4.8.2
+	ggsn_number	VARCHAR(15),
+	-- Chapter 2.4.9.2
+	gmlc_number	VARCHAR(15),
+	-- Chapter 2.4.23
+	smsc_number	VARCHAR(15),
+	-- Chapter 2.4.24
+	periodic_lu_tmr	INTEGER,
+	-- Chapter 2.13.115
+	periodic_rau_tau_tmr INTEGER,
+	-- Chapter 2.1.1.2: network access mode
+	nam_cs		BOOLEAN NOT NULL DEFAULT 1,
+	nam_ps		BOOLEAN NOT NULL DEFAULT 1,
+	-- Chapter 2.1.8
+	lmsi		INTEGER,
+
+	-- The below purged flags might not even be stored non-volatile,
+	-- refer to TS 23.012 Chapter 3.6.1.4
+	-- Chapter 2.7.5
+	ms_purged_cs	BOOLEAN NOT NULL DEFAULT 0,
+	-- Chapter 2.7.6
+	ms_purged_ps	BOOLEAN NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS subscriber_apn (
+	subscriber_id	INTEGER,		-- subscriber.id
+	apn		VARCHAR(256) NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS subscriber_multi_msisdn (
+-- Chapter 2.1.3
+	subscriber_id	INTEGER,		-- subscriber.id
+	msisdn		VARCHAR(15) NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS auc_2g (
+	subscriber_id	INTEGER PRIMARY KEY,	-- subscriber.id
+	algo_id_2g	INTEGER NOT NULL,	-- enum osmo_auth_algo value
+	ki		VARCHAR(32) NOT NULL	-- hex string: subscriber's secret key (128bit)
+);
+
+CREATE TABLE IF NOT EXISTS auc_3g (
+	subscriber_id	INTEGER PRIMARY KEY,	-- subscriber.id
+	algo_id_3g	INTEGER NOT NULL,	-- enum osmo_auth_algo value
+	k		VARCHAR(32) NOT NULL,	-- hex string: subscriber's secret key (128bit)
+	op		VARCHAR(32),		-- hex string: operator's secret key (128bit)
+	opc		VARCHAR(32),		-- hex string: derived from OP and K (128bit)
+	sqn		INTEGER NOT NULL DEFAULT 0,	-- sequence number of key usage
+	ind_bitlen	INTEGER NOT NULL DEFAULT 5	-- nr of index bits at lower SQN end
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_subscr_imsi ON subscriber (imsi);
diff --git a/sql/hlr_data.sql b/sql/hlr_data.sql
new file mode 100644
index 0000000..0767d48
--- /dev/null
+++ b/sql/hlr_data.sql
@@ -0,0 +1,13 @@
+
+-- 2G only subscriber
+INSERT INTO subscriber (id, imsi) VALUES (1, '901990000000001');
+INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki) VALUES (1, 1, '000102030405060708090a0b0c0d0e0f');
+
+-- 3G only subscriber
+INSERT INTO subscriber (id, imsi) VALUES (2, '901990000000002');
+INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, opc, sqn) VALUES (2, 5, '000102030405060708090a0b0c0d0e0f', '101112131415161718191a1b1c1d1e1f', 0);
+
+-- 2G + 3G subscriber
+INSERT INTO subscriber (id, imsi) VALUES (3, '901990000000003');
+INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki) VALUES (3, 1, '000102030405060708090a0b0c0d0e0f');
+INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, opc, sqn) VALUES (3, 5, '000102030405060708090a0b0c0d0e0f', '101112131415161718191a1b1c1d1e1f', 0);
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..4f167f0
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,124 @@
+SUBDIRS = gsupclient
+
+AM_CFLAGS = \
+	-Wall \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(LIBOSMOVTY_CFLAGS) \
+	$(LIBOSMOCTRL_CFLAGS) \
+	$(LIBOSMOABIS_CFLAGS) \
+	$(SQLITE3_CFLAGS) \
+	$(NULL)
+
+AM_CPPFLAGS = -I$(top_srcdir)/include \
+	$(NULL)
+
+EXTRA_DIST = \
+	populate_hlr_db.pl \
+	db_bootstrap.sed \
+	$(NULL)
+
+BUILT_SOURCES = \
+	db_bootstrap.h \
+	$(NULL)
+CLEANFILES = $(BUILT_SOURCES)
+
+noinst_HEADERS = \
+	auc.h \
+	db.h \
+	hlr.h \
+	luop.h \
+	gsup_router.h \
+	gsup_server.h \
+	logging.h \
+	rand.h \
+	ctrl.h \
+	hlr_vty.h \
+	hlr_vty_subscr.h \
+	hlr_ussd.h \
+	db_bootstrap.h \
+	$(NULL)
+
+bin_PROGRAMS = \
+	osmo-hlr \
+	osmo-hlr-db-tool \
+	osmo-euse-demo \
+	$(NULL)
+
+osmo_hlr_SOURCES = \
+	auc.c \
+	ctrl.c \
+	db.c \
+	luop.c \
+	db_auc.c \
+	db_hlr.c \
+	gsup_router.c \
+	gsup_server.c \
+	hlr.c \
+	logging.c \
+	rand_urandom.c \
+	hlr_vty.c \
+	hlr_vty_subscr.c \
+	gsup_send.c \
+	hlr_ussd.c \
+	$(NULL)
+
+osmo_hlr_LDADD = \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(LIBOSMOVTY_LIBS) \
+	$(LIBOSMOCTRL_LIBS) \
+	$(LIBOSMOABIS_LIBS) \
+	$(SQLITE3_LIBS) \
+	$(NULL)
+
+osmo_hlr_db_tool_SOURCES = \
+	hlr_db_tool.c \
+	db.c \
+	db_hlr.c \
+	logging.c \
+	rand_urandom.c \
+	dbd_decode_binary.c \
+	$(NULL)
+
+osmo_hlr_db_tool_LDADD = \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(SQLITE3_LIBS) \
+	$(NULL)
+
+db_test_SOURCES = \
+	auc.c \
+	db.c \
+	db_auc.c \
+	db_test.c \
+	logging.c \
+	rand_fake.c \
+	$(NULL)
+
+db_test_LDADD = \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(SQLITE3_LIBS) \
+	$(NULL)
+
+osmo_euse_demo_SOURCES = \
+	osmo-euse-demo.c \
+	$(NULL)
+
+osmo_euse_demo_LDADD = \
+	$(top_builddir)/src/gsupclient/libosmo-gsup-client.la \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+BOOTSTRAP_SQL = $(top_srcdir)/sql/hlr.sql
+
+db_bootstrap.h: $(BOOTSTRAP_SQL) $(srcdir)/db_bootstrap.sed
+	echo "/* DO NOT EDIT THIS FILE. It is generated from osmo-hlr.git/sql/hlr.sql */" > "$@"
+	echo "#pragma once" >> "$@"
+	echo "static const char *stmt_bootstrap_sql[] = {" >> "$@"
+	cat "$(BOOTSTRAP_SQL)" \
+		| sed -f "$(srcdir)/db_bootstrap.sed" \
+		>> "$@"
+	echo "};" >> "$@"
diff --git a/src/auc.c b/src/auc.c
new file mode 100644
index 0000000..79c3918
--- /dev/null
+++ b/src/auc.c
@@ -0,0 +1,185 @@
+/* (C) 2015 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 <string.h>
+#include <inttypes.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/crypt/auth.h>
+
+#include "logging.h"
+#include "rand.h"
+
+#define hexb(buf) osmo_hexdump_nospc((void*)buf, sizeof(buf))
+#define hex(buf,sz) osmo_hexdump_nospc((void*)buf, sz)
+
+/* compute given number of vectors using either aud2g or aud2g or a combination
+ * of both.  Handles re-synchronization if rand_auts and auts are set */
+int auc_compute_vectors(struct osmo_auth_vector *vec, unsigned int num_vec,
+			struct osmo_sub_auth_data *aud2g,
+			struct osmo_sub_auth_data *aud3g,
+			const uint8_t *rand_auts, const uint8_t *auts)
+{
+	unsigned int i;
+	uint8_t rand[16];
+	struct osmo_auth_vector vtmp;
+	int rc;
+
+	/* no need to iterate the log categories all the time */
+	int dbg = log_check_level(DAUC, LOGL_DEBUG);
+#define DBGP(args ...) if (dbg) DEBUGP(DAUC, ##args)
+#define DBGVB(member) DBGP("vector [%u]: " #member " = %s\n", \
+			   i, hexb(vec[i].member))
+#define DBGVV(fmt, member) DBGP("vector [%u]: " #member " = " fmt "\n", \
+			        i, vec[i].member)
+
+	if (aud2g && (aud2g->algo == OSMO_AUTH_ALG_NONE
+		      || aud2g->type == OSMO_AUTH_TYPE_NONE))
+		aud2g = NULL;
+	if (aud3g && (aud3g->algo == OSMO_AUTH_ALG_NONE
+		      || aud3g->type == OSMO_AUTH_TYPE_NONE))
+		aud3g = NULL;
+
+	if (!aud2g && !aud3g) {
+		LOGP(DAUC, LOGL_ERROR, "auc_compute_vectors() called"
+		     " with neither 2G nor 3G auth data available\n");
+		return -1;
+	}
+
+	if (aud2g && aud2g->type != OSMO_AUTH_TYPE_GSM) {
+		LOGP(DAUC, LOGL_ERROR, "auc_compute_vectors() called"
+		     " with non-2G auth data passed for aud2g arg\n");
+		return -1;
+	}
+
+	if (aud3g && aud3g->type != OSMO_AUTH_TYPE_UMTS) {
+		LOGP(DAUC, LOGL_ERROR, "auc_compute_vectors() called"
+		     " with non-3G auth data passed for aud3g arg\n");
+		return -1;
+	}
+
+	if ((rand_auts != NULL) != (auts != NULL)) {
+		LOGP(DAUC, LOGL_ERROR, "auc_compute_vectors() with only one"
+		     " of AUTS and AUTS_RAND given, need both or neither\n");
+		return -1;
+	}
+
+	if (auts && !aud3g) {
+		LOGP(DAUC, LOGL_ERROR, "auc_compute_vectors() with AUTS called"
+		     " but no 3G auth data passed\n");
+		return -1;
+	}
+
+	DBGP("Computing %d auth vector%s: %s%s\n",
+	     num_vec, num_vec == 1 ? "" : "s",
+	     aud3g? (aud2g? "3G + separate 2G"
+		     : "3G only (2G derived from 3G keys)")
+	     : "2G only",
+	     auts? ", with AUTS resync" : "");
+	if (aud3g) {
+		DBGP("3G: k = %s\n", hexb(aud3g->u.umts.k));
+		DBGP("3G: %s = %s\n",
+		     aud3g->u.umts.opc_is_op? "OP" : "opc",
+		     hexb(aud3g->u.umts.opc));
+		DBGP("3G: for sqn ind %u, previous sqn was %" PRIu64 "\n",
+		     aud3g->u.umts.ind, aud3g->u.umts.sqn);
+	}
+	if (aud2g)
+		DBGP("2G: ki = %s\n", hexb(aud2g->u.gsm.ki));
+
+	for (i = 0; i < num_vec; i++) {
+		rc = rand_get(rand, sizeof(rand));
+		if (rc != sizeof(rand)) {
+			LOGP(DAUC, LOGL_ERROR, "Unable to read %zu random "
+			     "bytes: rc=%d\n", sizeof(rand), rc);
+			goto out;
+		}
+		DBGP("vector [%u]: rand = %s\n", i, hexb(rand));
+
+		if (aud3g) {
+			/* 3G or 3G + 2G case */
+
+			/* Do AUTS only for the first vector or we would use
+			 * the same SQN for each following key. */
+			if ((i == 0) && auts) {
+				DBGP("vector [%u]: resync: auts = %s\n",
+				     i, hex(auts, 14));
+				DBGP("vector [%u]: resync: rand_auts = %s\n",
+				     i, hex(rand_auts, 16));
+
+				rc = osmo_auth_gen_vec_auts(vec+i, aud3g, auts,
+							    rand_auts, rand);
+			} else {
+				rc = osmo_auth_gen_vec(vec+i, aud3g, rand);
+			}
+			if (rc < 0) {
+				LOGP(DAUC, LOGL_ERROR, "Error in 3G vector "
+				     "generation: [%u]: rc = %d\n", i, rc);
+				goto out;
+			}
+			DBGP("vector [%u]: sqn = %" PRIu64 "\n",
+			     i, aud3g->u.umts.sqn);
+
+			DBGVB(autn);
+			DBGVB(ck);
+			DBGVB(ik);
+			DBGVB(res);
+			DBGVV("%u", res_len);
+
+			if (!aud2g) {
+				/* use the 2G tokens from 3G keys */
+				DBGP("vector [%u]: deriving 2G from 3G\n", i);
+				DBGVB(kc);
+				DBGVB(sres);
+				DBGVV("0x%x", auth_types);
+				continue;
+			}
+			/* calculate 2G separately */
+
+			DBGP("vector [%u]: calculating 2G separately\n", i);
+
+			rc = osmo_auth_gen_vec(&vtmp, aud2g, rand);
+			if (rc < 0) {
+				LOGP(DAUC, LOGL_ERROR, "Error in 2G vector"
+				     "generation: [%u]: rc = %d\n", i, rc);
+				goto out;
+			}
+			memcpy(&vec[i].kc, vtmp.kc, sizeof(vec[i].kc));
+			memcpy(&vec[i].sres, vtmp.sres, sizeof(vec[i].sres));
+			vec[i].auth_types |= OSMO_AUTH_TYPE_GSM;
+		} else {
+			/* 2G only case */
+			rc = osmo_auth_gen_vec(vec+i, aud2g, rand);
+			if (rc < 0) {
+				LOGP(DAUC, LOGL_ERROR, "Error in 2G vector "
+				     "generation: [%u]: rc = %d\n", i, rc);
+				goto out;
+			}
+		}
+
+		DBGVB(kc);
+		DBGVB(sres);
+		DBGVV("0x%x", auth_types);
+	}
+out:
+	return i;
+#undef DBGVV
+#undef DBGVB
+#undef DBGP
+}
diff --git a/src/auc.h b/src/auc.h
new file mode 100644
index 0000000..f5b6765
--- /dev/null
+++ b/src/auc.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <osmocom/crypt/auth.h>
+
+int auc_compute_vectors(struct osmo_auth_vector *vec, unsigned int num_vec,
+			struct osmo_sub_auth_data *aud2g,
+			struct osmo_sub_auth_data *aud3g,
+			const uint8_t *rand_auts, const uint8_t *auts);
diff --git a/src/ctrl.c b/src/ctrl.c
new file mode 100644
index 0000000..8ae9d7c
--- /dev/null
+++ b/src/ctrl.c
@@ -0,0 +1,408 @@
+/* OsmoHLR Control Interface implementation */
+
+/* (C) 2017 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Max Suraev <msuraev@sysmocom.de>
+ *
+ * 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 <stdbool.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include <osmocom/gsm/gsm23003.h>
+#include <osmocom/ctrl/ports.h>
+
+#include "hlr.h"
+#include "ctrl.h"
+#include "db.h"
+
+#define SEL_BY "by-"
+#define SEL_BY_IMSI SEL_BY "imsi-"
+#define SEL_BY_MSISDN SEL_BY "msisdn-"
+#define SEL_BY_ID SEL_BY "id-"
+
+#define hexdump_buf(buf) osmo_hexdump_nospc((void*)buf, sizeof(buf))
+
+static bool startswith(const char *str, const char *start)
+{
+	return strncmp(str, start, strlen(start)) == 0;
+}
+
+static int _get_subscriber(struct db_context *dbc,
+			   const char *by_selector,
+			   struct hlr_subscriber *subscr)
+{
+	const char *val;
+	if (startswith(by_selector, SEL_BY_IMSI)) {
+		val = by_selector + strlen(SEL_BY_IMSI);
+		if (!osmo_imsi_str_valid(val))
+			return -EINVAL;
+		return db_subscr_get_by_imsi(dbc, val, subscr);
+	}
+	if (startswith(by_selector, SEL_BY_MSISDN)) {
+		val = by_selector + strlen(SEL_BY_MSISDN);
+		if (!osmo_msisdn_str_valid(val))
+			return -EINVAL;
+		return db_subscr_get_by_msisdn(dbc, val, subscr);
+	}
+	if (startswith(by_selector, SEL_BY_ID)) {
+		int64_t id;
+		char *endptr;
+		val = by_selector + strlen(SEL_BY_ID);
+		if (*val == '+')
+			return -EINVAL;
+		errno = 0;
+		id = strtoll(val, &endptr, 10);
+		if (errno || *endptr)
+			return -EINVAL;
+		return db_subscr_get_by_id(dbc, id, subscr);
+	}
+	return -ENOTSUP;
+}
+
+static bool get_subscriber(struct db_context *dbc,
+			   const char *by_selector,
+			   struct hlr_subscriber *subscr,
+			   struct ctrl_cmd *cmd)
+{
+	int rc = _get_subscriber(dbc, by_selector, subscr);
+	switch (rc) {
+	case 0:
+		return true;
+	case -ENOTSUP:
+		cmd->reply = "Not a known subscriber 'by-xxx-' selector.";
+		return false;
+	case -EINVAL:
+		cmd->reply = "Invalid value part of 'by-xxx-value' selector.";
+		return false;
+	case -ENOENT:
+		cmd->reply = "No such subscriber.";
+		return false;
+	default:
+		cmd->reply = "An unknown error has occured during get_subscriber().";
+		return false;
+	}
+}
+
+/* Optimization: if a subscriber operation is requested by-imsi, just return
+ * the IMSI right back. */
+static const char *get_subscriber_imsi(struct db_context *dbc,
+				       const char *by_selector,
+				       struct ctrl_cmd *cmd)
+{
+	static struct hlr_subscriber subscr;
+
+	if (startswith(by_selector, SEL_BY_IMSI))
+		return by_selector + strlen(SEL_BY_IMSI);
+	if (!get_subscriber(dbc, by_selector, &subscr, cmd))
+		return NULL;
+	return subscr.imsi;
+}
+
+/* printf fmt and arg to completely omit a string if it is empty. */
+#define FMT_S "%s%s%s%s"
+#define ARG_S(name, val) \
+	(val) && *(val) ? "\n" : "", \
+	(val) && *(val) ? name : "", \
+	(val) && *(val) ? "\t" : "", \
+	(val) && *(val) ? (val) : "" \
+
+/* printf fmt and arg to completely omit bool of given value. */
+#define FMT_BOOL "%s"
+#define ARG_BOOL(name, val) \
+	val ? "\n" name "\t1" : "\n" name "\t0"
+
+static void print_subscr_info(struct ctrl_cmd *cmd,
+			      struct hlr_subscriber *subscr)
+{
+	ctrl_cmd_reply_printf(cmd,
+		"\nid\t%"PRIu64
+		FMT_S
+		FMT_S
+		FMT_BOOL
+		FMT_BOOL
+		FMT_S
+		FMT_S
+		FMT_S
+		FMT_BOOL
+		FMT_BOOL
+		"\nperiodic_lu_timer\t%u"
+		"\nperiodic_rau_tau_timer\t%u"
+		"\nlmsi\t%08x"
+		,
+		subscr->id,
+		ARG_S("imsi", subscr->imsi),
+		ARG_S("msisdn", subscr->msisdn),
+		ARG_BOOL("nam_cs", subscr->nam_cs),
+		ARG_BOOL("nam_ps", subscr->nam_ps),
+		ARG_S("vlr_number", subscr->vlr_number),
+		ARG_S("sgsn_number", subscr->sgsn_number),
+		ARG_S("sgsn_address", subscr->sgsn_address),
+		ARG_BOOL("ms_purged_cs", subscr->ms_purged_cs),
+		ARG_BOOL("ms_purged_ps", subscr->ms_purged_ps),
+		subscr->periodic_lu_timer,
+		subscr->periodic_rau_tau_timer,
+		subscr->lmsi
+		);
+}
+
+static void print_subscr_info_aud2g(struct ctrl_cmd *cmd, struct osmo_sub_auth_data *aud)
+{
+	if (aud->algo == OSMO_AUTH_ALG_NONE)
+		return;
+	ctrl_cmd_reply_printf(cmd,
+		"\naud2g.algo\t%s"
+		"\naud2g.ki\t%s"
+		,
+		osmo_auth_alg_name(aud->algo),
+		hexdump_buf(aud->u.gsm.ki));
+}
+
+static void print_subscr_info_aud3g(struct ctrl_cmd *cmd, struct osmo_sub_auth_data *aud)
+{
+	if (aud->algo == OSMO_AUTH_ALG_NONE)
+		return;
+	ctrl_cmd_reply_printf(cmd,
+		"\naud3g.algo\t%s"
+		"\naud3g.k\t%s"
+		,
+		osmo_auth_alg_name(aud->algo),
+		hexdump_buf(aud->u.umts.k));
+	/* hexdump uses a static string buffer, hence only one hexdump per
+	 * printf(). */
+	ctrl_cmd_reply_printf(cmd,
+		"\naud3g.%s\t%s"
+		"\naud3g.ind_bitlen\t%u"
+		"\naud3g.sqn\t%"PRIu64
+		,
+		aud->u.umts.opc_is_op? "op" : "opc",
+		hexdump_buf(aud->u.umts.opc),
+		aud->u.umts.ind_bitlen,
+		aud->u.umts.sqn);
+}
+
+CTRL_CMD_DEFINE_RO(subscr_info, "info");
+static int get_subscr_info(struct ctrl_cmd *cmd, void *data)
+{
+	struct hlr_subscriber subscr;
+	struct hlr *hlr = data;
+	const char *by_selector = cmd->node;
+
+	if (!get_subscriber(hlr->dbc, by_selector, &subscr, cmd))
+		return CTRL_CMD_ERROR;
+
+	print_subscr_info(cmd, &subscr);
+
+	return CTRL_CMD_REPLY;
+}
+
+CTRL_CMD_DEFINE_RO(subscr_info_aud, "info-aud");
+static int get_subscr_info_aud(struct ctrl_cmd *cmd, void *data)
+{
+	const char *imsi;
+	struct osmo_sub_auth_data aud2g;
+	struct osmo_sub_auth_data aud3g;
+	struct hlr *hlr = data;
+	const char *by_selector = cmd->node;
+	int rc;
+
+	imsi = get_subscriber_imsi(hlr->dbc, by_selector, cmd);
+	if (!imsi)
+		return CTRL_CMD_ERROR;
+
+	rc = db_get_auth_data(hlr->dbc, imsi, &aud2g, &aud3g, NULL);
+
+	switch (rc) {
+	case 0:
+		break;
+	case -ENOENT:
+	case -ENOKEY:
+		/* No auth data found, tell the print*() functions about it. */
+		aud2g.algo = OSMO_AUTH_ALG_NONE;
+		aud3g.algo = OSMO_AUTH_ALG_NONE;
+		break;
+	default:
+		cmd->reply = "Error retrieving authentication data.";
+		return CTRL_CMD_ERROR;
+	}
+
+	print_subscr_info_aud2g(cmd, &aud2g);
+	print_subscr_info_aud3g(cmd, &aud3g);
+
+	return CTRL_CMD_REPLY;
+}
+
+CTRL_CMD_DEFINE_RO(subscr_info_all, "info-all");
+static int get_subscr_info_all(struct ctrl_cmd *cmd, void *data)
+{
+	struct hlr_subscriber subscr;
+	struct osmo_sub_auth_data aud2g;
+	struct osmo_sub_auth_data aud3g;
+	struct hlr *hlr = data;
+	const char *by_selector = cmd->node;
+	int rc;
+
+	if (!get_subscriber(hlr->dbc, by_selector, &subscr, cmd))
+		return CTRL_CMD_ERROR;
+
+	rc = db_get_auth_data(hlr->dbc, subscr.imsi, &aud2g, &aud3g, NULL);
+
+	switch (rc) {
+	case 0:
+		break;
+	case -ENOENT:
+	case -ENOKEY:
+		/* No auth data found, tell the print*() functions about it. */
+		aud2g.algo = OSMO_AUTH_ALG_NONE;
+		aud3g.algo = OSMO_AUTH_ALG_NONE;
+		break;
+	default:
+		cmd->reply = "Error retrieving authentication data.";
+		return CTRL_CMD_ERROR;
+	}
+
+	print_subscr_info(cmd, &subscr);
+	print_subscr_info_aud2g(cmd, &aud2g);
+	print_subscr_info_aud3g(cmd, &aud3g);
+
+	return CTRL_CMD_REPLY;
+}
+
+static int verify_subscr_cs_ps_enabled(struct ctrl_cmd *cmd, const char *value, void *data)
+{
+	if (!value || !*value
+	    || (strcmp(value, "0") && strcmp(value, "1")))
+		return 1;
+	return 0;
+}
+
+static int get_subscr_cs_ps_enabled(struct ctrl_cmd *cmd, void *data,
+				    bool is_ps)
+{
+	struct hlr_subscriber subscr;
+	struct hlr *hlr = data;
+	const char *by_selector = cmd->node;
+
+	if (!get_subscriber(hlr->dbc, by_selector, &subscr, cmd))
+		return CTRL_CMD_ERROR;
+
+	cmd->reply = (is_ps ? subscr.nam_ps : subscr.nam_cs)
+		     ? "1" : "0";
+	return CTRL_CMD_REPLY;
+}
+
+static int set_subscr_cs_ps_enabled(struct ctrl_cmd *cmd, void *data,
+				    bool is_ps)
+{
+	const char *imsi;
+	struct hlr *hlr = data;
+	const char *by_selector = cmd->node;
+
+	imsi = get_subscriber_imsi(hlr->dbc, by_selector, cmd);
+	if (!imsi)
+		return CTRL_CMD_ERROR;
+	if (db_subscr_nam(hlr->dbc, imsi, strcmp(cmd->value, "1") == 0, is_ps))
+		return CTRL_CMD_ERROR;
+	cmd->reply = "OK";
+	return CTRL_CMD_REPLY;
+}
+
+CTRL_CMD_DEFINE(subscr_ps_enabled, "ps-enabled");
+static int verify_subscr_ps_enabled(struct ctrl_cmd *cmd, const char *value, void *data)
+{
+	return verify_subscr_cs_ps_enabled(cmd, value, data);
+}
+static int get_subscr_ps_enabled(struct ctrl_cmd *cmd, void *data)
+{
+	return get_subscr_cs_ps_enabled(cmd, data, true);
+}
+static int set_subscr_ps_enabled(struct ctrl_cmd *cmd, void *data)
+{
+	return set_subscr_cs_ps_enabled(cmd, data, true);
+}
+
+CTRL_CMD_DEFINE(subscr_cs_enabled, "cs-enabled");
+static int verify_subscr_cs_enabled(struct ctrl_cmd *cmd, const char *value, void *data)
+{
+	return verify_subscr_cs_ps_enabled(cmd, value, data);
+}
+static int get_subscr_cs_enabled(struct ctrl_cmd *cmd, void *data)
+{
+	return get_subscr_cs_ps_enabled(cmd, data, false);
+}
+static int set_subscr_cs_enabled(struct ctrl_cmd *cmd, void *data)
+{
+	return set_subscr_cs_ps_enabled(cmd, data, false);
+}
+
+int hlr_ctrl_cmds_install()
+{
+	int rc = 0;
+
+	rc |= ctrl_cmd_install(CTRL_NODE_SUBSCR_BY, &cmd_subscr_info);
+	rc |= ctrl_cmd_install(CTRL_NODE_SUBSCR_BY, &cmd_subscr_info_aud);
+	rc |= ctrl_cmd_install(CTRL_NODE_SUBSCR_BY, &cmd_subscr_info_all);
+	rc |= ctrl_cmd_install(CTRL_NODE_SUBSCR_BY, &cmd_subscr_ps_enabled);
+	rc |= ctrl_cmd_install(CTRL_NODE_SUBSCR_BY, &cmd_subscr_cs_enabled);
+
+	return rc;
+}
+
+static int hlr_ctrl_node_lookup(void *data, vector vline, int *node_type,
+				void **node_data, int *i)
+{
+	const char *token = vector_slot(vline, *i);
+
+	switch (*node_type) {
+	case CTRL_NODE_ROOT:
+		if (strcmp(token, "subscriber") != 0)
+			return 0;
+		*node_data = NULL;
+		*node_type = CTRL_NODE_SUBSCR;
+		break;
+	case CTRL_NODE_SUBSCR:
+		if (!startswith(token, "by-"))
+			return 0;
+		*node_data = (void*)token;
+		*node_type = CTRL_NODE_SUBSCR_BY;
+		break;
+	default:
+		return 0;
+	}
+
+	return 1;
+}
+
+struct ctrl_handle *hlr_controlif_setup(struct hlr *hlr)
+{
+	int rc;
+	struct ctrl_handle *hdl = ctrl_interface_setup_dynip2(hlr,
+							      hlr->ctrl_bind_addr,
+							      OSMO_CTRL_PORT_HLR,
+							      hlr_ctrl_node_lookup,
+							      _LAST_CTRL_NODE_HLR);
+	if (!hdl)
+		return NULL;
+
+	rc = hlr_ctrl_cmds_install();
+	if (rc) /* FIXME: close control interface? */
+		return NULL;
+
+	return hdl;
+}
diff --git a/src/ctrl.h b/src/ctrl.h
new file mode 100644
index 0000000..3f9ba3f
--- /dev/null
+++ b/src/ctrl.h
@@ -0,0 +1,34 @@
+/* OsmoHLR Control Interface implementation */
+
+/* (C) 2017 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Max Suraev <msuraev@sysmocom.de>
+ *
+ * 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/>.
+ *
+ */
+
+#pragma once
+
+#include <osmocom/ctrl/control_if.h>
+
+enum hlr_ctrl_node {
+	CTRL_NODE_SUBSCR = _LAST_CTRL_NODE,
+	CTRL_NODE_SUBSCR_BY,
+	_LAST_CTRL_NODE_HLR
+};
+
+int hlr_ctrl_cmds_install();
+struct ctrl_handle *hlr_controlif_setup(struct hlr *hlr);
diff --git a/src/db.c b/src/db.c
new file mode 100644
index 0000000..4b0577f
--- /dev/null
+++ b/src/db.c
@@ -0,0 +1,298 @@
+/* (C) 2015 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 <osmocom/core/utils.h>
+
+#include <stdbool.h>
+#include <sqlite3.h>
+#include <string.h>
+
+#include "logging.h"
+#include "db.h"
+#include "db_bootstrap.h"
+
+#define SEL_COLUMNS \
+	"id," \
+	"imsi," \
+	"msisdn," \
+	"vlr_number," \
+	"sgsn_number," \
+	"sgsn_address," \
+	"periodic_lu_tmr," \
+	"periodic_rau_tau_tmr," \
+	"nam_cs," \
+	"nam_ps," \
+	"lmsi," \
+	"ms_purged_cs," \
+	"ms_purged_ps"
+
+static const char *stmt_sql[] = {
+	[DB_STMT_SEL_BY_IMSI] = "SELECT " SEL_COLUMNS " FROM subscriber WHERE imsi = ?",
+	[DB_STMT_SEL_BY_MSISDN] = "SELECT " SEL_COLUMNS " FROM subscriber WHERE msisdn = ?",
+	[DB_STMT_SEL_BY_ID] = "SELECT " SEL_COLUMNS " FROM subscriber WHERE id = ?",
+	[DB_STMT_UPD_VLR_BY_ID] = "UPDATE subscriber SET vlr_number = $number WHERE id = $subscriber_id",
+	[DB_STMT_UPD_SGSN_BY_ID] = "UPDATE subscriber SET sgsn_number = $number WHERE id = $subscriber_id",
+	[DB_STMT_AUC_BY_IMSI] =
+		"SELECT id, algo_id_2g, ki, algo_id_3g, k, op, opc, sqn, ind_bitlen"
+		" FROM subscriber"
+		" LEFT JOIN auc_2g ON auc_2g.subscriber_id = subscriber.id"
+		" LEFT JOIN auc_3g ON auc_3g.subscriber_id = subscriber.id"
+		" WHERE imsi = $imsi",
+	[DB_STMT_AUC_UPD_SQN] = "UPDATE auc_3g SET sqn = $sqn WHERE subscriber_id = $subscriber_id",
+	[DB_STMT_UPD_PURGE_CS_BY_IMSI] = "UPDATE subscriber SET ms_purged_cs = $val WHERE imsi = $imsi",
+	[DB_STMT_UPD_PURGE_PS_BY_IMSI] = "UPDATE subscriber SET ms_purged_ps = $val WHERE imsi = $imsi",
+	[DB_STMT_UPD_NAM_CS_BY_IMSI] = "UPDATE subscriber SET nam_cs = $val WHERE imsi = $imsi",
+	[DB_STMT_UPD_NAM_PS_BY_IMSI] = "UPDATE subscriber SET nam_ps = $val WHERE imsi = $imsi",
+	[DB_STMT_SUBSCR_CREATE] = "INSERT INTO subscriber (imsi) VALUES ($imsi)",
+	[DB_STMT_DEL_BY_ID] = "DELETE FROM subscriber WHERE id = $subscriber_id",
+	[DB_STMT_SET_MSISDN_BY_IMSI] = "UPDATE subscriber SET msisdn = $msisdn WHERE imsi = $imsi",
+	[DB_STMT_AUC_2G_INSERT] =
+		"INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki)"
+		" VALUES($subscriber_id, $algo_id_2g, $ki)",
+	[DB_STMT_AUC_2G_DELETE] = "DELETE FROM auc_2g WHERE subscriber_id = $subscriber_id",
+	[DB_STMT_AUC_3G_INSERT] =
+		"INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, op, opc, ind_bitlen)"
+		" VALUES($subscriber_id, $algo_id_3g, $k, $op, $opc, $ind_bitlen)",
+	[DB_STMT_AUC_3G_DELETE] = "DELETE FROM auc_3g WHERE subscriber_id = $subscriber_id",
+};
+
+static void sql3_error_log_cb(void *arg, int err_code, const char *msg)
+{
+	LOGP(DDB, LOGL_ERROR, "(%d) %s\n", err_code, msg);
+}
+
+static void sql3_sql_log_cb(void *arg, sqlite3 *s3, const char *stmt, int type)
+{
+	switch (type) {
+	case 0:
+		LOGP(DDB, LOGL_DEBUG, "Opened database\n");
+		break;
+	case 1:
+		LOGP(DDB, LOGL_DEBUG, "%s\n", stmt);
+		break;
+	case 2:
+		LOGP(DDB, LOGL_DEBUG, "Closed database\n");
+		break;
+	default:
+		LOGP(DDB, LOGL_DEBUG, "Unknown %d\n", type);
+		break;
+	}
+}
+
+/* remove bindings and reset statement to be re-executed */
+void db_remove_reset(sqlite3_stmt *stmt)
+{
+	sqlite3_clear_bindings(stmt);
+	/* sqlite3_reset() just repeats an error code already evaluated during sqlite3_step(). */
+	/* coverity[CHECKED_RETURN] */
+	sqlite3_reset(stmt);
+}
+
+/** bind text arg and do proper cleanup in case of failure. If param_name is
+ * NULL, bind to the first parameter (useful for SQL statements that have only
+ * one parameter). */
+bool db_bind_text(sqlite3_stmt *stmt, const char *param_name, const char *text)
+{
+	int rc;
+	int idx = param_name ? sqlite3_bind_parameter_index(stmt, param_name) : 1;
+	if (idx < 1) {
+		LOGP(DDB, LOGL_ERROR, "Error composing SQL, cannot bind parameter '%s'\n",
+		     param_name);
+		return false;
+	}
+	rc = sqlite3_bind_text(stmt, idx, text, -1, SQLITE_STATIC);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Error binding text to SQL parameter %s: %d\n",
+		     param_name ? param_name : "#1", rc);
+		db_remove_reset(stmt);
+		return false;
+	}
+	return true;
+}
+
+/** bind int arg and do proper cleanup in case of failure. If param_name is
+ * NULL, bind to the first parameter (useful for SQL statements that have only
+ * one parameter). */
+bool db_bind_int(sqlite3_stmt *stmt, const char *param_name, int nr)
+{
+	int rc;
+	int idx = param_name ? sqlite3_bind_parameter_index(stmt, param_name) : 1;
+	if (idx < 1) {
+		LOGP(DDB, LOGL_ERROR, "Error composing SQL, cannot bind parameter '%s'\n",
+		     param_name);
+		return false;
+	}
+	rc = sqlite3_bind_int(stmt, idx, nr);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Error binding int64 to SQL parameter %s: %d\n",
+		     param_name ? param_name : "#1", rc);
+		db_remove_reset(stmt);
+		return false;
+	}
+	return true;
+}
+
+/** bind int64 arg and do proper cleanup in case of failure. If param_name is
+ * NULL, bind to the first parameter (useful for SQL statements that have only
+ * one parameter). */
+bool db_bind_int64(sqlite3_stmt *stmt, const char *param_name, int64_t nr)
+{
+	int rc;
+	int idx = param_name ? sqlite3_bind_parameter_index(stmt, param_name) : 1;
+	if (idx < 1) {
+		LOGP(DDB, LOGL_ERROR, "Error composing SQL, cannot bind parameter '%s'\n",
+		     param_name);
+		return false;
+	}
+	rc = sqlite3_bind_int64(stmt, idx, nr);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Error binding int64 to SQL parameter %s: %d\n",
+		     param_name ? param_name : "#1", rc);
+		db_remove_reset(stmt);
+		return false;
+	}
+	return true;
+}
+
+void db_close(struct db_context *dbc)
+{
+	unsigned int i;
+	int rc;
+
+	for (i = 0; i < ARRAY_SIZE(dbc->stmt); i++) {
+		/* it is ok to call finalize on NULL */
+		sqlite3_finalize(dbc->stmt[i]);
+	}
+
+	/* Ask sqlite3 to close DB */
+	rc = sqlite3_close(dbc->db);
+	if (rc != SQLITE_OK) { /* Make sure it's actually closed! */
+		LOGP(DDB, LOGL_ERROR, "Couldn't close database: (rc=%d) %s\n",
+			rc, sqlite3_errmsg(dbc->db));
+	}
+
+	talloc_free(dbc);
+}
+
+static int db_bootstrap(struct db_context *dbc)
+{
+	int i;
+	for (i = 0; i < ARRAY_SIZE(stmt_bootstrap_sql); i++) {
+		int rc;
+		sqlite3_stmt *stmt;
+
+		rc = sqlite3_prepare_v2(dbc->db, stmt_bootstrap_sql[i], -1,
+					&stmt, NULL);
+		if (rc != SQLITE_OK) {
+			LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n",
+			     stmt_bootstrap_sql[i]);
+			return rc;
+		}
+
+		/* execute the statement */
+		rc = sqlite3_step(stmt);
+		db_remove_reset(stmt);
+		sqlite3_finalize(stmt);
+		if (rc != SQLITE_DONE) {
+			LOGP(DDB, LOGL_ERROR, "Cannot bootstrap database: SQL error: (%d) %s,"
+			     " during stmt '%s'",
+			     rc, sqlite3_errmsg(dbc->db),
+			     stmt_bootstrap_sql[i]);
+			return rc;
+		}
+	}
+	return SQLITE_OK;
+}
+
+struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite_logging)
+{
+	struct db_context *dbc = talloc_zero(ctx, struct db_context);
+	unsigned int i;
+	int rc;
+	bool has_sqlite_config_sqllog = false;
+
+	LOGP(DDB, LOGL_NOTICE, "using database: %s\n", fname);
+	LOGP(DDB, LOGL_INFO, "Compiled against SQLite3 lib version %s\n", SQLITE_VERSION);
+	LOGP(DDB, LOGL_INFO, "Running with SQLite3 lib version %s\n", sqlite3_libversion());
+
+	dbc->fname = talloc_strdup(dbc, fname);
+
+	for (i = 0; i < 0xfffff; i++) {
+		const char *o = sqlite3_compileoption_get(i);
+		if (!o)
+			break;
+		LOGP(DDB, LOGL_DEBUG, "SQLite3 compiled with '%s'\n", o);
+		if (!strcmp(o, "ENABLE_SQLLOG"))
+			has_sqlite_config_sqllog = true;
+	}
+
+	if (enable_sqlite_logging) {
+		rc = sqlite3_config(SQLITE_CONFIG_LOG, sql3_error_log_cb, NULL);
+		if (rc != SQLITE_OK)
+			LOGP(DDB, LOGL_NOTICE, "Unable to set SQLite3 error log callback\n");
+	}
+
+	if (has_sqlite_config_sqllog) {
+		rc = sqlite3_config(SQLITE_CONFIG_SQLLOG, sql3_sql_log_cb, NULL);
+		if (rc != SQLITE_OK)
+			LOGP(DDB, LOGL_NOTICE, "Unable to set SQLite3 SQL log callback\n");
+	} else
+			LOGP(DDB, LOGL_DEBUG, "Not setting SQL log callback:"
+			     " SQLite3 compiled without support for it\n");
+
+	rc = sqlite3_open(dbc->fname, &dbc->db);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Unable to open DB; rc = %d\n", rc);
+		talloc_free(dbc);
+		return NULL;
+	}
+
+	/* enable extended result codes */
+	rc = sqlite3_extended_result_codes(dbc->db, 1);
+	if (rc != SQLITE_OK)
+		LOGP(DDB, LOGL_ERROR, "Unable to enable SQLite3 extended result codes\n");
+
+	char *err_msg;
+	rc = sqlite3_exec(dbc->db, "PRAGMA journal_mode=WAL; PRAGMA synchonous = NORMAL;", 0, 0, &err_msg);
+	if (rc != SQLITE_OK)
+		LOGP(DDB, LOGL_ERROR, "Unable to set Write-Ahead Logging: %s\n",
+			err_msg);
+
+	rc = db_bootstrap(dbc);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Failed to bootstrap DB: (rc=%d) %s\n",
+			rc, sqlite3_errmsg(dbc->db));
+		goto out_free;
+	}
+
+	/* prepare all SQL statements */
+	for (i = 0; i < ARRAY_SIZE(dbc->stmt); i++) {
+		rc = sqlite3_prepare_v2(dbc->db, stmt_sql[i], -1,
+					&dbc->stmt[i], NULL);
+		if (rc != SQLITE_OK) {
+			LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", stmt_sql[i]);
+			goto out_free;
+		}
+	}
+
+	return dbc;
+out_free:
+	db_close(dbc);
+	return NULL;
+}
diff --git a/src/db.h b/src/db.h
new file mode 100644
index 0000000..956b5ed
--- /dev/null
+++ b/src/db.h
@@ -0,0 +1,142 @@
+#pragma once
+
+#include <stdbool.h>
+#include <sqlite3.h>
+
+struct hlr;
+
+enum stmt_idx {
+	DB_STMT_SEL_BY_IMSI,
+	DB_STMT_SEL_BY_MSISDN,
+	DB_STMT_SEL_BY_ID,
+	DB_STMT_UPD_VLR_BY_ID,
+	DB_STMT_UPD_SGSN_BY_ID,
+	DB_STMT_AUC_BY_IMSI,
+	DB_STMT_AUC_UPD_SQN,
+	DB_STMT_UPD_PURGE_CS_BY_IMSI,
+	DB_STMT_UPD_PURGE_PS_BY_IMSI,
+	DB_STMT_UPD_NAM_PS_BY_IMSI,
+	DB_STMT_UPD_NAM_CS_BY_IMSI,
+	DB_STMT_SUBSCR_CREATE,
+	DB_STMT_DEL_BY_ID,
+	DB_STMT_SET_MSISDN_BY_IMSI,
+	DB_STMT_AUC_2G_INSERT,
+	DB_STMT_AUC_2G_DELETE,
+	DB_STMT_AUC_3G_INSERT,
+	DB_STMT_AUC_3G_DELETE,
+	_NUM_DB_STMT
+};
+
+struct db_context {
+	char *fname;
+	sqlite3 *db;
+	sqlite3_stmt *stmt[_NUM_DB_STMT];
+};
+
+void db_remove_reset(sqlite3_stmt *stmt);
+bool db_bind_text(sqlite3_stmt *stmt, const char *param_name, const char *text);
+bool db_bind_int(sqlite3_stmt *stmt, const char *param_name, int nr);
+bool db_bind_int64(sqlite3_stmt *stmt, const char *param_name, int64_t nr);
+void db_close(struct db_context *dbc);
+struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite3_logging);
+
+#include <osmocom/crypt/auth.h>
+
+/* obtain the authentication data for a given imsi */
+int db_get_auth_data(struct db_context *dbc, const char *imsi,
+		     struct osmo_sub_auth_data *aud2g,
+		     struct osmo_sub_auth_data *aud3g,
+		     int64_t *subscr_id);
+
+int db_update_sqn(struct db_context *dbc, int64_t id,
+		      uint64_t new_sqn);
+
+int db_get_auc(struct db_context *dbc, const char *imsi,
+	       unsigned int auc_3g_ind, struct osmo_auth_vector *vec,
+	       unsigned int num_vec, const uint8_t *rand_auts,
+	       const uint8_t *auts);
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/gsm/protocol/gsm_23_003.h>
+
+/* TODO: Get this from somewhere? */
+#define GT_MAX_DIGITS	15
+
+struct hlr_subscriber {
+	struct llist_head list;
+
+	int64_t		id;
+	char		imsi[GSM23003_IMSI_MAX_DIGITS+1];
+	char		msisdn[GT_MAX_DIGITS+1];
+	/* imeisv? */
+	char		vlr_number[32];
+	char		sgsn_number[32];
+	char		sgsn_address[GT_MAX_DIGITS+1];
+	/* ggsn number + address */
+	/* gmlc number */
+	/* smsc number */
+	uint32_t	periodic_lu_timer;
+	uint32_t	periodic_rau_tau_timer;
+	bool		nam_cs;
+	bool		nam_ps;
+	uint32_t	lmsi;
+	bool		ms_purged_cs;
+	bool		ms_purged_ps;
+};
+
+/* Like struct osmo_sub_auth_data, but the keys are in hexdump representation.
+ * This is useful because SQLite requires them in hexdump format, and callers
+ * like the VTY and CTRL interface also have them available as hexdump to begin
+ * with. In the binary format, a VTY command would first need to hexparse,
+ * after which the db function would again hexdump, copying to separate
+ * buffers. The roundtrip can be saved by providing char* to begin with. */
+struct sub_auth_data_str {
+	enum osmo_sub_auth_type type;
+	enum osmo_auth_algo algo;
+	union {
+		struct {
+			const char *opc;
+			const char *k;
+			uint64_t sqn;
+			int opc_is_op;
+			unsigned int ind_bitlen;
+		} umts;
+		struct {
+			const char *ki;
+		} gsm;
+	} u;
+};
+
+int db_subscr_create(struct db_context *dbc, const char *imsi);
+int db_subscr_delete_by_id(struct db_context *dbc, int64_t subscr_id);
+
+int db_subscr_update_msisdn_by_imsi(struct db_context *dbc, const char *imsi,
+				    const char *msisdn);
+int db_subscr_update_aud_by_id(struct db_context *dbc, int64_t subscr_id,
+			       const struct sub_auth_data_str *aud);
+
+int db_subscr_get_by_imsi(struct db_context *dbc, const char *imsi,
+			  struct hlr_subscriber *subscr);
+int db_subscr_get_by_msisdn(struct db_context *dbc, const char *msisdn,
+			    struct hlr_subscriber *subscr);
+int db_subscr_get_by_id(struct db_context *dbc, int64_t id,
+			struct hlr_subscriber *subscr);
+int db_subscr_nam(struct db_context *dbc, const char *imsi, bool nam_val, bool is_ps);
+int db_subscr_lu(struct db_context *dbc, int64_t subscr_id,
+		 const char *vlr_or_sgsn_number, bool is_ps);
+
+int db_subscr_purge(struct db_context *dbc, const char *by_imsi,
+		    bool purge_val, bool is_ps);
+
+int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps);
+
+/*! Call sqlite3_column_text() and copy result to a char[].
+ * \param[out] buf  A char[] used as sizeof() arg(!) and osmo_strlcpy() target.
+ * \param[in] stmt  An sqlite3_stmt*.
+ * \param[in] idx   Index in stmt's returned columns.
+ */
+#define copy_sqlite3_text_to_buf(buf, stmt, idx) \
+	do { \
+		const char *_txt = (const char *) sqlite3_column_text(stmt, idx); \
+		osmo_strlcpy(buf, _txt, sizeof(buf)); \
+	} while (0)
diff --git a/src/db_auc.c b/src/db_auc.c
new file mode 100644
index 0000000..5fb5e3a
--- /dev/null
+++ b/src/db_auc.c
@@ -0,0 +1,228 @@
+/* (C) 2015 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 <string.h>
+#include <inttypes.h>
+#include <errno.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/crypt/auth.h>
+
+#include <sqlite3.h>
+
+#include "logging.h"
+#include "db.h"
+#include "auc.h"
+#include "rand.h"
+
+#define LOGAUC(imsi, level, fmt, args ...)	LOGP(DAUC, level, "IMSI='%s': " fmt, imsi, ## args)
+
+/* update the SQN for a given subscriber ID */
+int db_update_sqn(struct db_context *dbc, int64_t subscr_id, uint64_t new_sqn)
+{
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_AUC_UPD_SQN];
+	int rc;
+	int ret = 0;
+
+	if (!db_bind_int64(stmt, "$sqn", new_sqn))
+		return -EIO;
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot update SQN for subscriber ID=%"PRId64
+		     ": SQL error: (%d) %s\n",
+		     subscr_id, rc, sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot update SQN for subscriber ID=%"PRId64
+		     ": no auc_3g entry for such subscriber\n", subscr_id);
+		ret = -ENOENT;
+	} else if (rc != 1) {
+		LOGP(DAUC, LOGL_ERROR, "Update SQN for subscriber ID=%"PRId64
+		     ": SQL modified %d rows (expected 1)\n", subscr_id, rc);
+		ret = -EIO;
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+/* obtain the authentication data for a given imsi
+ * returns 0 for success, negative value on error:
+ * -ENOENT if the IMSI is not known, -ENOKEY if the IMSI is known but has no auth data,
+ * -EIO on db failure */
+int db_get_auth_data(struct db_context *dbc, const char *imsi,
+		     struct osmo_sub_auth_data *aud2g,
+		     struct osmo_sub_auth_data *aud3g,
+		     int64_t *subscr_id)
+{
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_AUC_BY_IMSI];
+	int ret = 0;
+	int rc;
+
+	memset(aud2g, 0, sizeof(*aud2g));
+	memset(aud3g, 0, sizeof(*aud3g));
+
+	if (!db_bind_text(stmt, "$imsi", imsi))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc == SQLITE_DONE) {
+		LOGAUC(imsi, LOGL_INFO, "No such subscriber\n");
+		ret = -ENOENT;
+		goto out;
+	} else if (rc != SQLITE_ROW) {
+		LOGAUC(imsi, LOGL_ERROR, "Error executing SQL: %d\n", rc);
+		ret = -EIO;
+		goto out;
+	}
+
+	/* as an optimization, we retrieve the subscriber ID, to ensure we can
+	 * update the SQN later without having to go back via a JOIN with the
+	 * subscriber table. */
+	if (subscr_id)
+		*subscr_id = sqlite3_column_int64(stmt, 0);
+
+	/* obtain result values using sqlite3_column_*() */
+	if (sqlite3_column_type(stmt, 1) == SQLITE_INTEGER) {
+		/* we do have some 2G authentication data */
+		const uint8_t *ki;
+
+		aud2g->algo = sqlite3_column_int(stmt, 1);
+		ki = sqlite3_column_text(stmt, 2);
+#if 0
+		if (sqlite3_column_bytes(stmt, 2) != sizeof(aud2g->u.gsm.ki)) {
+			LOGAUC(imsi, LOGL_ERROR, "Error reading Ki: %d\n", rc);
+			goto end_2g;
+		}
+#endif
+		osmo_hexparse((void*)ki, (void*)&aud2g->u.gsm.ki, sizeof(aud2g->u.gsm.ki));
+		aud2g->type = OSMO_AUTH_TYPE_GSM;
+	} else
+		LOGAUC(imsi, LOGL_DEBUG, "No 2G Auth Data\n");
+//end_2g:
+	if (sqlite3_column_type(stmt, 3) == SQLITE_INTEGER) {
+		/* we do have some 3G authentication data */
+		const uint8_t *k, *op, *opc;
+
+		aud3g->algo = sqlite3_column_int(stmt, 3);
+		k = sqlite3_column_text(stmt, 4);
+		if (!k) {
+			LOGAUC(imsi, LOGL_ERROR, "Error reading K: %d\n", rc);
+			ret = -EIO;
+			goto out;
+		}
+		osmo_hexparse((void*)k, (void*)&aud3g->u.umts.k, sizeof(aud3g->u.umts.k));
+		/* UMTS Subscribers can have either OP or OPC */
+		op = sqlite3_column_text(stmt, 5);
+		if (!op) {
+			opc = sqlite3_column_text(stmt, 6);
+			if (!opc) {
+				LOGAUC(imsi, LOGL_ERROR, "Error reading OPC: %d\n", rc);
+				ret = -EIO;
+				goto out;
+			}
+			osmo_hexparse((void*)opc, (void*)&aud3g->u.umts.opc,
+					sizeof(aud3g->u.umts.opc));
+			aud3g->u.umts.opc_is_op = 0;
+		} else {
+			osmo_hexparse((void*)op, (void*)&aud3g->u.umts.opc,
+					sizeof(aud3g->u.umts.opc));
+			aud3g->u.umts.opc_is_op = 1;
+		}
+		aud3g->u.umts.sqn = sqlite3_column_int64(stmt, 7);
+		aud3g->u.umts.ind_bitlen = sqlite3_column_int(stmt, 8);
+		/* FIXME: amf? */
+		aud3g->type = OSMO_AUTH_TYPE_UMTS;
+	} else
+		LOGAUC(imsi, LOGL_DEBUG, "No 3G Auth Data\n");
+
+	if (aud2g->type == 0 && aud3g->type == 0)
+		ret = -ENOKEY;
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+/* return number of vectors generated, negative value on error:
+ * -ENOENT if the IMSI is not known, -ENOKEY if the IMSI is known but has no auth data,
+ * -EIO on db failure */
+int db_get_auc(struct db_context *dbc, const char *imsi,
+	       unsigned int auc_3g_ind, struct osmo_auth_vector *vec,
+	       unsigned int num_vec, const uint8_t *rand_auts,
+	       const uint8_t *auts)
+{
+	struct osmo_sub_auth_data aud2g, aud3g;
+	int64_t subscr_id;
+	int ret = 0;
+	int rc;
+
+	rc = db_get_auth_data(dbc, imsi, &aud2g, &aud3g, &subscr_id);
+	if (rc)
+		return rc;
+
+	aud3g.u.umts.ind = auc_3g_ind;
+	if (aud3g.type == OSMO_AUTH_TYPE_UMTS
+	    && aud3g.u.umts.ind >= (1U << aud3g.u.umts.ind_bitlen)) {
+		LOGAUC(imsi, LOGL_NOTICE, "3G auth: SQN's IND bitlen %u is"
+		       " too small to hold an index of %u. Truncating. This"
+		       " may cause numerous additional AUTS resyncing.\n",
+		       aud3g.u.umts.ind_bitlen, aud3g.u.umts.ind);
+		aud3g.u.umts.ind &= (1U << aud3g.u.umts.ind_bitlen) - 1;
+	}
+
+	LOGAUC(imsi, LOGL_DEBUG, "Calling to generate %u vectors\n", num_vec);
+	rc = auc_compute_vectors(vec, num_vec, &aud2g, &aud3g, rand_auts, auts);
+	if (rc < 0) {
+		num_vec = 0;
+		ret = -1;
+	} else {
+		num_vec = rc;
+		ret = num_vec;
+	}
+	LOGAUC(imsi, LOGL_INFO, "Generated %u vectors\n", num_vec);
+
+	/* Update SQN in database, as needed */
+	if (aud3g.algo) {
+		LOGAUC(imsi, LOGL_DEBUG, "Updating SQN=%" PRIu64 " in DB\n",
+		       aud3g.u.umts.sqn);
+		rc = db_update_sqn(dbc, subscr_id, aud3g.u.umts.sqn);
+		/* don't tell caller we generated any triplets in case of
+		 * update error */
+		if (rc < 0) {
+			LOGAUC(imsi, LOGL_ERROR, "Error updating SQN: %d\n", rc);
+			num_vec = 0;
+			ret = -1;
+		}
+	}
+
+	return ret;
+}
diff --git a/src/db_bootstrap.sed b/src/db_bootstrap.sed
new file mode 100644
index 0000000..60b8243
--- /dev/null
+++ b/src/db_bootstrap.sed
@@ -0,0 +1,25 @@
+# Input to this is sql/hlr.sql.
+#
+# We want each SQL statement line wrapped in "...\n", and each end (";") to
+# become a comma:
+#
+#   SOME SQL COMMAND (
+#     that may span )
+#   MULTIPLE LINES;
+#   MORE;
+#
+# -->
+#
+#   "SOME SQL COMMAND (\n"
+#   "  that may span )\n"
+#   "MULTIPLE LINES\n",   <--note the comma here
+#   "MORE\n",
+#
+# just replacing ';' with '\n,' won't work, since sed is bad in printing
+# multiple lines. Also, how to input newlines to sed is not portable across
+# platforms.
+
+# Match excluding a trailing ';' as \1, keep any trailing ';' in \2
+s/^\(.*[^;]\)\(;\|\)$/"\1\\n"\2/
+# Replace trailing ';' as ','
+s/;$/,/
diff --git a/src/db_hlr.c b/src/db_hlr.c
new file mode 100644
index 0000000..c4d4974
--- /dev/null
+++ b/src/db_hlr.c
@@ -0,0 +1,717 @@
+/* (C) 2015 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 <string.h>
+#include <errno.h>
+#include <inttypes.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/crypt/auth.h>
+#include <osmocom/gsm/gsm23003.h>
+
+#include <sqlite3.h>
+
+#include "logging.h"
+#include "hlr.h"
+#include "db.h"
+#include "gsup_server.h"
+#include "luop.h"
+
+#define LOGHLR(imsi, level, fmt, args ...)	LOGP(DAUC, level, "IMSI='%s': " fmt, imsi, ## args)
+
+/*! Add new subscriber record to the HLR database.
+ * \param[in,out] dbc  database context.
+ * \param[in] imsi  ASCII string of IMSI digits, is validated.
+ * \returns 0 on success, -EINVAL on invalid IMSI, -EIO on database error.
+ */
+int db_subscr_create(struct db_context *dbc, const char *imsi)
+{
+	sqlite3_stmt *stmt;
+	int rc;
+
+	if (!osmo_imsi_str_valid(imsi)) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot create subscriber: invalid IMSI: '%s'\n",
+		     imsi);
+		return -EINVAL;
+	}
+
+	stmt = dbc->stmt[DB_STMT_SUBSCR_CREATE];
+
+	if (!db_bind_text(stmt, "$imsi", imsi))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	db_remove_reset(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGHLR(imsi, LOGL_ERROR, "Cannot create subscriber: SQL error: (%d) %s\n",
+		       rc, sqlite3_errmsg(dbc->db));
+		return -EIO;
+	}
+
+	return 0;
+}
+
+/*! Completely delete a subscriber record from the HLR database.
+ * Also remove authentication data.
+ * Future todo: also drop from all other database tables, which aren't used yet
+ * at the time of writing this.
+ * \param[in,out] dbc  database context.
+ * \param[in] subscr_id  ID of the subscriber in the HLR db.
+ * \returns if the subscriber was found and removed, -EIO on database error,
+ *          -ENOENT if no such subscriber data exists.
+ */
+int db_subscr_delete_by_id(struct db_context *dbc, int64_t subscr_id)
+{
+	int rc;
+	struct sub_auth_data_str aud;
+	int ret = 0;
+
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_DEL_BY_ID];
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR,
+		       "Cannot delete subscriber ID=%"PRId64": SQL error: (%d) %s\n",
+		       subscr_id, rc, sqlite3_errmsg(dbc->db));
+		db_remove_reset(stmt);
+		return -EIO;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot delete: no such subscriber: ID=%"PRId64"\n",
+		     subscr_id);
+		ret = -ENOENT;
+	} else if (rc != 1) {
+		LOGP(DAUC, LOGL_ERROR, "Delete subscriber ID=%"PRId64
+		     ": SQL modified %d rows (expected 1)\n", subscr_id, rc);
+		ret = -EIO;
+	}
+	db_remove_reset(stmt);
+
+	/* make sure to remove authentication data for this subscriber id, for
+	 * both 2G and 3G. */
+
+	aud = (struct sub_auth_data_str){
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+	rc = db_subscr_update_aud_by_id(dbc, subscr_id, &aud);
+	if (ret == -ENOENT && !rc)
+		ret = 0;
+
+	aud = (struct sub_auth_data_str){
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+	rc = db_subscr_update_aud_by_id(dbc, subscr_id, &aud);
+	if (ret == -ENOENT && !rc)
+		ret = 0;
+
+	return ret;
+}
+
+/*! Set a subscriber's MSISDN in the HLR database.
+ * \param[in,out] dbc  database context.
+ * \param[in] imsi  ASCII string of IMSI digits.
+ * \param[in] msisdn  ASCII string of MSISDN digits.
+ * \returns 0 on success, -EINVAL in case of invalid MSISDN string, -EIO on
+ *          database failure, -ENOENT if no such subscriber exists.
+ */
+int db_subscr_update_msisdn_by_imsi(struct db_context *dbc, const char *imsi,
+				    const char *msisdn)
+{
+	int rc;
+	int ret = 0;
+
+	if (!osmo_msisdn_str_valid(msisdn)) {
+		LOGHLR(imsi, LOGL_ERROR,
+		       "Cannot update subscriber: invalid MSISDN: '%s'\n",
+		       msisdn);
+		return -EINVAL;
+	}
+
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_SET_MSISDN_BY_IMSI];
+
+	if (!db_bind_text(stmt, "$imsi", imsi))
+		return -EIO;
+	if (!db_bind_text(stmt, "$msisdn", msisdn))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGHLR(imsi, LOGL_ERROR,
+		       "Cannot update subscriber's MSISDN: SQL error: (%d) %s\n",
+		       rc, sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot update MSISDN: no such subscriber: IMSI='%s'\n",
+		     imsi);
+		ret = -ENOENT;
+		goto out;
+	} else if (rc != 1) {
+		LOGHLR(imsi, LOGL_ERROR, "Update MSISDN: SQL modified %d rows (expected 1)\n", rc);
+		ret = -EIO;
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+
+}
+
+/*! Insert or update 2G or 3G authentication tokens in the database.
+ * If aud->type is OSMO_AUTH_TYPE_GSM, the auc_2g table entry for the
+ * subscriber will be added or modified; if aud->algo is OSMO_AUTH_ALG_NONE,
+ * however, the auc_2g entry for the subscriber is deleted. If aud->type is
+ * OSMO_AUTH_TYPE_UMTS, the auc_3g table is updated; again, if aud->algo is
+ * OSMO_AUTH_ALG_NONE, the auc_3g entry is deleted.
+ * \param[in,out] dbc  database context.
+ * \param[in] subscr_id  DB ID of the subscriber.
+ * \param[in] aud  Pointer to new auth data (in ASCII string form).
+ * \returns 0 on success, -EINVAL for invalid aud, -ENOENT for unknown
+ *          subscr_id, -EIO for database errors.
+ */
+int db_subscr_update_aud_by_id(struct db_context *dbc, int64_t subscr_id,
+			       const struct sub_auth_data_str *aud)
+{
+	sqlite3_stmt *stmt_del;
+	sqlite3_stmt *stmt_ins;
+	sqlite3_stmt *stmt;
+	const char *label;
+	int rc;
+	int ret = 0;
+
+	switch (aud->type) {
+	case OSMO_AUTH_TYPE_GSM:
+		label = "auc_2g";
+		stmt_del = dbc->stmt[DB_STMT_AUC_2G_DELETE];
+		stmt_ins = dbc->stmt[DB_STMT_AUC_2G_INSERT];
+
+		switch (aud->algo) {
+		case OSMO_AUTH_ALG_NONE:
+		case OSMO_AUTH_ALG_COMP128v1:
+		case OSMO_AUTH_ALG_COMP128v2:
+		case OSMO_AUTH_ALG_COMP128v3:
+		case OSMO_AUTH_ALG_XOR:
+			break;
+		case OSMO_AUTH_ALG_MILENAGE:
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " auth algo not suited for 2G: %s\n",
+			     osmo_auth_alg_name(aud->algo));
+			return -EINVAL;
+		default:
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " Unknown auth algo: %d\n", aud->algo);
+			return -EINVAL;
+		}
+
+		if (aud->algo == OSMO_AUTH_ALG_NONE)
+			break;
+		if (!osmo_is_hexstr(aud->u.gsm.ki, 32, 32, true)) {
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " Invalid KI: '%s'\n", aud->u.gsm.ki);
+			return -EINVAL;
+		}
+		break;
+
+	case OSMO_AUTH_TYPE_UMTS:
+		label = "auc_3g";
+		stmt_del = dbc->stmt[DB_STMT_AUC_3G_DELETE];
+		stmt_ins = dbc->stmt[DB_STMT_AUC_3G_INSERT];
+		switch (aud->algo) {
+		case OSMO_AUTH_ALG_NONE:
+		case OSMO_AUTH_ALG_MILENAGE:
+			break;
+		case OSMO_AUTH_ALG_COMP128v1:
+		case OSMO_AUTH_ALG_COMP128v2:
+		case OSMO_AUTH_ALG_COMP128v3:
+		case OSMO_AUTH_ALG_XOR:
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " auth algo not suited for 3G: %s\n",
+			     osmo_auth_alg_name(aud->algo));
+			return -EINVAL;
+		default:
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " Unknown auth algo: %d\n", aud->algo);
+			return -EINVAL;
+		}
+
+		if (aud->algo == OSMO_AUTH_ALG_NONE)
+			break;
+		if (!osmo_is_hexstr(aud->u.umts.k, 32, 32, true)) {
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " Invalid K: '%s'\n", aud->u.umts.k);
+			return -EINVAL;
+		}
+		if (!osmo_is_hexstr(aud->u.umts.opc, 32, 32, true)) {
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " Invalid OP/OPC: '%s'\n", aud->u.umts.opc);
+			return -EINVAL;
+		}
+		if (aud->u.umts.ind_bitlen > OSMO_MILENAGE_IND_BITLEN_MAX) {
+			LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+			     " Invalid ind_bitlen: %d\n", aud->u.umts.ind_bitlen);
+			return -EINVAL;
+		}
+		break;
+	default:
+		LOGP(DAUC, LOGL_ERROR, "Cannot update auth tokens:"
+		     " unknown auth type: %d\n", aud->type);
+		return -EINVAL;
+	}
+
+	stmt = stmt_del;
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR,
+		     "Cannot delete %s row: SQL error: (%d) %s\n",
+		     label, rc, sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc)
+		/* Leave "no such entry" logging to the caller -- during
+		 * db_subscr_delete_by_id(), we call this to make sure it is
+		 * empty, and no entry is not an error then.*/
+		ret = -ENOENT;
+	else if (rc != 1) {
+		LOGP(DAUC, LOGL_ERROR, "Delete subscriber ID=%"PRId64
+		     " from %s: SQL modified %d rows (expected 1)\n",
+		     subscr_id, label, rc);
+		ret = -EIO;
+	}
+
+	db_remove_reset(stmt);
+
+	/* Error situation? Return now. */
+	if (ret && ret != -ENOENT)
+		return ret;
+
+	/* Just delete requested? */
+	if (aud->algo == OSMO_AUTH_ALG_NONE)
+		return ret;
+
+	/* Don't return -ENOENT if inserting new data. */
+	ret = 0;
+
+	/* Insert new row */
+	stmt = stmt_ins;
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
+		return -EIO;
+
+	switch (aud->type) {
+	case OSMO_AUTH_TYPE_GSM:
+		if (!db_bind_int(stmt, "$algo_id_2g", aud->algo))
+			return -EIO;
+		if (!db_bind_text(stmt, "$ki", aud->u.gsm.ki))
+			return -EIO;
+		break;
+	case OSMO_AUTH_TYPE_UMTS:
+		if (!db_bind_int(stmt, "$algo_id_3g", aud->algo))
+			return -EIO;
+		if (!db_bind_text(stmt, "$k", aud->u.umts.k))
+			return -EIO;
+		if (!db_bind_text(stmt, "$op",
+				  aud->u.umts.opc_is_op ? aud->u.umts.opc : NULL))
+			return -EIO;
+		if (!db_bind_text(stmt, "$opc",
+				  aud->u.umts.opc_is_op ? NULL : aud->u.umts.opc))
+			return -EIO;
+		if (!db_bind_int(stmt, "$ind_bitlen", aud->u.umts.ind_bitlen))
+			return -EIO;
+		break;
+	default:
+		OSMO_ASSERT(false);
+	}
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR,
+		     "Cannot insert %s row: SQL error: (%d) %s\n",
+		     label, rc, sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+/* Common code for db_subscr_get_by_*() functions. */
+static int db_sel(struct db_context *dbc, sqlite3_stmt *stmt, struct hlr_subscriber *subscr,
+		  const char **err)
+{
+	int rc;
+	int ret = 0;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc == SQLITE_DONE) {
+		ret = -ENOENT;
+		goto out;
+	}
+	if (rc != SQLITE_ROW) {
+		ret = -EIO;
+		goto out;
+	}
+
+	if (!subscr)
+		goto out;
+
+	*subscr = (struct hlr_subscriber){};
+
+	/* obtain the various columns */
+	subscr->id = sqlite3_column_int64(stmt, 0);
+	copy_sqlite3_text_to_buf(subscr->imsi, stmt, 1);
+	copy_sqlite3_text_to_buf(subscr->msisdn, stmt, 2);
+	/* FIXME: These should all be BLOBs as they might contain NUL */
+	copy_sqlite3_text_to_buf(subscr->vlr_number, stmt, 3);
+	copy_sqlite3_text_to_buf(subscr->sgsn_number, stmt, 4);
+	copy_sqlite3_text_to_buf(subscr->sgsn_address, stmt, 5);
+	subscr->periodic_lu_timer = sqlite3_column_int(stmt, 6);
+	subscr->periodic_rau_tau_timer = sqlite3_column_int(stmt, 7);
+	subscr->nam_cs = sqlite3_column_int(stmt, 8);
+	subscr->nam_ps = sqlite3_column_int(stmt, 9);
+	subscr->lmsi = sqlite3_column_int(stmt, 10);
+	subscr->ms_purged_cs = sqlite3_column_int(stmt, 11);
+	subscr->ms_purged_ps = sqlite3_column_int(stmt, 12);
+
+out:
+	db_remove_reset(stmt);
+
+	switch (ret) {
+	case 0:
+		*err = NULL;
+		break;
+	case -ENOENT:
+		*err = "No such subscriber";
+		break;
+	default:
+		*err = sqlite3_errmsg(dbc->db);
+		break;
+	}
+	return ret;
+}
+
+/*! Retrieve subscriber data from the HLR database.
+ * \param[in,out] dbc  database context.
+ * \param[in] imsi  ASCII string of IMSI digits.
+ * \param[out] subscr  place retrieved data in this struct.
+ * \returns 0 on success, -ENOENT if no such subscriber was found, -EIO on
+ *          database error.
+ */
+int db_subscr_get_by_imsi(struct db_context *dbc, const char *imsi,
+			  struct hlr_subscriber *subscr)
+{
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_SEL_BY_IMSI];
+	const char *err;
+	int rc;
+
+	if (!db_bind_text(stmt, NULL, imsi))
+		return -EIO;
+
+	rc = db_sel(dbc, stmt, subscr, &err);
+	if (rc)
+		LOGP(DAUC, LOGL_ERROR, "Cannot read subscriber from db: IMSI='%s': %s\n",
+		     imsi, err);
+	return rc;
+}
+
+/*! Retrieve subscriber data from the HLR database.
+ * \param[in,out] dbc  database context.
+ * \param[in] msisdn  ASCII string of MSISDN digits.
+ * \param[out] subscr  place retrieved data in this struct.
+ * \returns 0 on success, -ENOENT if no such subscriber was found, -EIO on
+ *          database error.
+ */
+int db_subscr_get_by_msisdn(struct db_context *dbc, const char *msisdn,
+			    struct hlr_subscriber *subscr)
+{
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_SEL_BY_MSISDN];
+	const char *err;
+	int rc;
+
+	if (!db_bind_text(stmt, NULL, msisdn))
+		return -EIO;
+
+	rc = db_sel(dbc, stmt, subscr, &err);
+	if (rc)
+		LOGP(DAUC, LOGL_ERROR, "Cannot read subscriber from db: MSISDN='%s': %s\n",
+		     msisdn, err);
+	return rc;
+}
+
+/*! Retrieve subscriber data from the HLR database.
+ * \param[in,out] dbc  database context.
+ * \param[in] id  ID of the subscriber in the HLR db.
+ * \param[out] subscr  place retrieved data in this struct.
+ * \returns 0 on success, -ENOENT if no such subscriber was found, -EIO on
+ *          database error.
+ */
+int db_subscr_get_by_id(struct db_context *dbc, int64_t id,
+			struct hlr_subscriber *subscr)
+{
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_SEL_BY_ID];
+	const char *err;
+	int rc;
+
+	if (!db_bind_int64(stmt, NULL, id))
+		return -EIO;
+
+	rc = db_sel(dbc, stmt, subscr, &err);
+	if (rc)
+		LOGP(DAUC, LOGL_ERROR, "Cannot read subscriber from db: ID=%"PRId64": %s\n",
+		     id, err);
+	return rc;
+}
+
+/*! You should use hlr_subscr_nam() instead; enable or disable PS or CS for a
+ * subscriber without notifying GSUP clients.
+ * \param[in,out] dbc  database context.
+ * \param[in] imsi  ASCII string of IMSI digits.
+ * \param[in] nam_val True to enable CS/PS, false to disable.
+ * \param[in] is_ps  when true, set nam_ps, else set nam_cs.
+ * \returns 0 on success, -ENOENT when the given IMSI does not exist, -EIO on
+ *          database errors.
+ */
+int db_subscr_nam(struct db_context *dbc, const char *imsi, bool nam_val, bool is_ps)
+{
+	sqlite3_stmt *stmt;
+	int rc;
+	int ret = 0;
+
+	stmt = dbc->stmt[is_ps ? DB_STMT_UPD_NAM_PS_BY_IMSI
+			       : DB_STMT_UPD_NAM_CS_BY_IMSI];
+
+	if (!db_bind_text(stmt, "$imsi", imsi))
+		return -EIO;
+	if (!db_bind_int(stmt, "$val", nam_val ? 1 : 0))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGHLR(imsi, LOGL_ERROR, "%s %s: SQL error: %s\n",
+		       nam_val ? "enable" : "disable",
+		       is_ps ? "PS" : "CS",
+		       sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot %s %s: no such subscriber: IMSI='%s'\n",
+		     nam_val ? "enable" : "disable",
+		     is_ps ? "PS" : "CS",
+		     imsi);
+		ret = -ENOENT;
+		goto out;
+	} else if (rc != 1) {
+		LOGHLR(imsi, LOGL_ERROR, "%s %s: SQL modified %d rows (expected 1)\n",
+		       nam_val ? "enable" : "disable",
+		       is_ps ? "PS" : "CS",
+		       rc);
+		ret = -EIO;
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+/*! Record a Location Updating in the database.
+ * \param[in,out] dbc  database context.
+ * \param[in] subscr_id  ID of the subscriber in the HLR db.
+ * \param[in] vlr_or_sgsn_number  ASCII string of identifier digits.
+ * \param[in] is_ps  when true, set sgsn_number, else set vlr_number.
+ * \returns 0 on success, -ENOENT when the given subscriber does not exist,
+ *         -EIO on database errors.
+ */
+int db_subscr_lu(struct db_context *dbc, int64_t subscr_id,
+		 const char *vlr_or_sgsn_number, bool is_ps)
+{
+	sqlite3_stmt *stmt;
+	int rc, ret = 0;
+
+	stmt = dbc->stmt[is_ps ? DB_STMT_UPD_SGSN_BY_ID
+			       : DB_STMT_UPD_VLR_BY_ID];
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
+		return -EIO;
+
+	if (!db_bind_text(stmt, "$number", vlr_or_sgsn_number))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR, "Update %s number for subscriber ID=%"PRId64": SQL Error: %s\n",
+		     is_ps? "SGSN" : "VLR", subscr_id, sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot update %s number for subscriber ID=%"PRId64
+		     ": no such subscriber\n",
+		     is_ps? "SGSN" : "VLR", subscr_id);
+		ret = -ENOENT;
+	} else if (rc != 1) {
+		LOGP(DAUC, LOGL_ERROR, "Update %s number for subscriber ID=%"PRId64
+		       ": SQL modified %d rows (expected 1)\n",
+		       is_ps? "SGSN" : "VLR", subscr_id, rc);
+		ret = -EIO;
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+/*! Set the ms_purged_cs or ms_purged_ps values in the database.
+ * \param[in,out] dbc  database context.
+ * \param[in] by_imsi  ASCII string of IMSI digits.
+ * \param[in] purge_val  true to purge, false to un-purge.
+ * \param[in] is_ps  when true, set ms_purged_ps, else set ms_purged_cs.
+ * \returns 0 on success, -ENOENT when the given IMSI does not exist, -EIO on
+ *          database errors.
+ */
+int db_subscr_purge(struct db_context *dbc, const char *by_imsi,
+		    bool purge_val, bool is_ps)
+{
+	sqlite3_stmt *stmt;
+	int rc, ret = 0;
+
+	stmt = dbc->stmt[is_ps ? DB_STMT_UPD_PURGE_PS_BY_IMSI
+			       : DB_STMT_UPD_PURGE_CS_BY_IMSI];
+
+	if (!db_bind_text(stmt, "$imsi", by_imsi))
+		return -EIO;
+	if (!db_bind_int(stmt, "$val", purge_val ? 1 : 0))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR, "%s %s: SQL error: %s\n",
+		     purge_val ? "purge" : "un-purge",
+		     is_ps ? "PS" : "CS",
+		     sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DAUC, LOGL_ERROR, "Cannot %s %s: no such subscriber: IMSI='%s'\n",
+		     purge_val ? "purge" : "un-purge",
+		     is_ps ? "PS" : "CS",
+		     by_imsi);
+		ret = -ENOENT;
+		goto out;
+	} else if (rc != 1) {
+		LOGHLR(by_imsi, LOGL_ERROR, "%s %s: SQL modified %d rows (expected 1)\n",
+		       purge_val ? "purge" : "un-purge",
+		       is_ps ? "PS" : "CS",
+		       rc);
+		ret = -EIO;
+	}
+
+out:
+	db_remove_reset(stmt);
+
+	return ret;
+}
+
+/*! Update nam_cs/nam_ps in the db and trigger notifications to GSUP clients.
+ * \param[in,out] hlr  Global hlr context.
+ * \param[in] subscr   Subscriber from a fresh db_subscr_get_by_*() call.
+ * \param[in] nam_val  True to enable CS/PS, false to disable.
+ * \param[in] is_ps    True to enable/disable PS, false for CS.
+ * \returns 0 on success, ENOEXEC if there is no need to change, a negative
+ *          value on error.
+ */
+int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps)
+{
+	int rc;
+        struct lu_operation *luop;
+        struct osmo_gsup_conn *co;
+	bool is_val = is_ps? subscr->nam_ps : subscr->nam_cs;
+
+	if (is_val == nam_val) {
+		LOGHLR(subscr->imsi, LOGL_DEBUG, "Already has the requested value when asked to %s %s\n",
+		       nam_val ? "enable" : "disable", is_ps ? "PS" : "CS");
+		return ENOEXEC;
+	}
+
+	rc = db_subscr_nam(hlr->dbc, subscr->imsi, nam_val, is_ps);
+	if (rc)
+		return rc > 0? -rc : rc;
+
+	/* If we're disabling, send a notice out to the GSUP client that is
+	 * responsible. Otherwise no need. */
+	if (nam_val)
+		return 0;
+
+	/* FIXME: only send to single SGSN where latest update for IMSI came from */
+	llist_for_each_entry(co, &hlr->gs->clients, list) {
+		luop = lu_op_alloc_conn(co);
+		if (!luop) {
+			LOGHLR(subscr->imsi, LOGL_ERROR,
+			       "Cannot notify GSUP client, cannot allocate lu_operation,"
+			       " for %s:%u\n",
+			       co && co->conn && co->conn->server? co->conn->server->addr : "unset",
+			       co && co->conn && co->conn->server? co->conn->server->port : 0);
+			continue;
+		}
+		luop->subscr = *subscr;
+		lu_op_tx_del_subscr_data(luop);
+		lu_op_free(luop);
+	}
+	return 0;
+}
diff --git a/src/dbd_decode_binary.c b/src/dbd_decode_binary.c
new file mode 100644
index 0000000..e1a98ad
--- /dev/null
+++ b/src/dbd_decode_binary.c
@@ -0,0 +1,42 @@
+/* This function is blatantly copied from libdbi, from
+ * https://sourceforge.net/p/libdbi/libdbi/ci/master/tree/src/dbd_helper.c
+ * to save having to depend on the entire libdbi just for KI BLOB decoding.
+ */
+
+/*
+ * libdbi - database independent abstraction layer for C.
+ * Copyright (C) 2001-2003, David Parker and Mark Tobenkin.
+ * http://libdbi.sourceforge.net
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library 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
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * 
+ * $Id: dbd_helper.c,v 1.44 2011/08/09 11:14:14 mhoenicka Exp $
+ */
+
+#include <sys/types.h>
+
+size_t _dbd_decode_binary(const unsigned char *in, unsigned char *out){
+  int i, e;
+  unsigned char c;
+  e = *(in++);
+  i = 0;
+  while( (c = *(in++))!=0 ){
+    if( c==1 ){
+      c = *(in++) - 1;
+    }
+    out[i++] = c + e;
+  }
+  return (size_t)i;
+}
diff --git a/src/gsup_router.c b/src/gsup_router.c
new file mode 100644
index 0000000..6038ee6
--- /dev/null
+++ b/src/gsup_router.c
@@ -0,0 +1,90 @@
+/* (C) 2016 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 <osmocom/core/linuxlist.h>
+#include <osmocom/core/talloc.h>
+
+#include "logging.h"
+#include "gsup_server.h"
+
+struct gsup_route {
+	struct llist_head list;
+
+	uint8_t *addr;
+	struct osmo_gsup_conn *conn;
+};
+
+/* find a route for the given address */
+struct osmo_gsup_conn *gsup_route_find(struct osmo_gsup_server *gs,
+					const uint8_t *addr, size_t addrlen)
+{
+	struct gsup_route *gr;
+
+	llist_for_each_entry(gr, &gs->routes, list) {
+		if (talloc_total_size(gr->addr) == addrlen &&
+		    !memcmp(gr->addr, addr, addrlen))
+			return gr->conn;
+	}
+	return NULL;
+}
+
+/* add a new route for the given address to the given conn */
+int gsup_route_add(struct osmo_gsup_conn *conn, const uint8_t *addr, size_t addrlen)
+{
+	struct gsup_route *gr;
+
+	/* Check if we already have a route for this address */
+	if (gsup_route_find(conn->server, addr, addrlen))
+		return -EEXIST;
+
+	/* allocate new route and populate it */
+	gr = talloc_zero(conn->server, struct gsup_route);
+	if (!gr)
+		return -ENOMEM;
+
+	LOGP(DMAIN, LOGL_INFO, "Adding GSUP route for %s\n", addr);
+
+	gr->addr = talloc_memdup(gr, addr, addrlen);
+	gr->conn = conn;
+	llist_add_tail(&gr->list, &conn->server->routes);
+
+	return 0;
+}
+
+/* delete all routes for the given connection */
+int gsup_route_del_conn(struct osmo_gsup_conn *conn)
+{
+	struct gsup_route *gr, *gr2;
+	unsigned int num_deleted = 0;
+
+	llist_for_each_entry_safe(gr, gr2, &conn->server->routes, list) {
+		if (gr->conn == conn) {
+			LOGP(DMAIN, LOGL_INFO, "Removing GSUP route for %s (GSUP disconnect)\n",
+			     gr->addr);
+			llist_del(&gr->list);
+			talloc_free(gr);
+			num_deleted++;
+		}
+	}
+
+	return num_deleted;
+}
diff --git a/src/gsup_router.h b/src/gsup_router.h
new file mode 100644
index 0000000..282531d
--- /dev/null
+++ b/src/gsup_router.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <stdint.h>
+#include "gsup_server.h"
+
+struct osmo_gsup_conn *gsup_route_find(struct osmo_gsup_server *gs,
+					const uint8_t *addr, size_t addrlen);
+
+/* add a new route for the given address to the given conn */
+int gsup_route_add(struct osmo_gsup_conn *conn, const uint8_t *addr, size_t addrlen);
+
+/* delete all routes for the given connection */
+int gsup_route_del_conn(struct osmo_gsup_conn *conn);
+
+int osmo_gsup_addr_send(struct osmo_gsup_server *gs,
+			const uint8_t *addr, size_t addrlen,
+			struct msgb *msg);
diff --git a/src/gsup_send.c b/src/gsup_send.c
new file mode 100644
index 0000000..b2c4e02
--- /dev/null
+++ b/src/gsup_send.c
@@ -0,0 +1,45 @@
+/* (C) 2018 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * 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/>.
+ *
+ */
+
+/* This is kept separate to be able to override the actual sending functions from unit tests. */
+
+#include <errno.h>
+
+#include "gsup_server.h"
+#include "gsup_router.h"
+
+#include <osmocom/core/logging.h>
+
+/* Send a msgb to a given address using routing */
+int osmo_gsup_addr_send(struct osmo_gsup_server *gs,
+			const uint8_t *addr, size_t addrlen,
+			struct msgb *msg)
+{
+	struct osmo_gsup_conn *conn;
+
+	conn = gsup_route_find(gs, addr, addrlen);
+	if (!conn) {
+		DEBUGP(DLGSUP, "Cannot find route for addr %s\n", addr);
+		msgb_free(msg);
+		return -ENODEV;
+	}
+
+	return osmo_gsup_conn_send(conn, msg);
+}
+
diff --git a/src/gsup_server.c b/src/gsup_server.c
new file mode 100644
index 0000000..e75bbd7
--- /dev/null
+++ b/src/gsup_server.c
@@ -0,0 +1,414 @@
+/* (C) 2016 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 <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/abis/ipa.h>
+#include <osmocom/abis/ipaccess.h>
+#include <osmocom/gsm/gsm48_ie.h>
+#include <osmocom/gsm/apn.h>
+
+#include "gsup_server.h"
+#include "gsup_router.h"
+
+static void osmo_gsup_server_send(struct osmo_gsup_conn *conn,
+			     int proto_ext, struct msgb *msg_tx)
+{
+	ipa_prepend_header_ext(msg_tx, proto_ext);
+	ipa_msg_push_header(msg_tx, IPAC_PROTO_OSMO);
+	ipa_server_conn_send(conn->conn, msg_tx);
+}
+
+int osmo_gsup_conn_send(struct osmo_gsup_conn *conn, struct msgb *msg)
+{
+	if (!conn) {
+		msgb_free(msg);
+		return -ENOTCONN;
+	}
+
+	osmo_gsup_server_send(conn, IPAC_PROTO_EXT_GSUP, msg);
+
+	return 0;
+}
+
+static int osmo_gsup_conn_oap_handle(struct osmo_gsup_conn *conn,
+				struct msgb *msg_rx)
+{
+#if 0
+	int rc;
+	struct msgb *msg_tx;
+	rc = oap_handle(&conn->oap_state, msg_rx, &msg_tx);
+	msgb_free(msg_rx);
+	if (rc < 0)
+		return rc;
+
+	if (msg_tx)
+		osmo_gsup_conn_send(conn, IPAC_PROTO_EXT_OAP, msg_tx);
+#endif
+	return 0;
+}
+
+/* Data from a given client has arrived over the socket */
+static int osmo_gsup_server_read_cb(struct ipa_server_conn *conn,
+			       struct msgb *msg)
+{
+	struct ipaccess_head *hh = (struct ipaccess_head *) msg->data;
+	struct ipaccess_head_ext *he = (struct ipaccess_head_ext *) msgb_l2(msg);
+	struct osmo_gsup_conn *clnt = (struct osmo_gsup_conn *)conn->data;
+	int rc;
+
+	msg->l2h = &hh->data[0];
+
+	if (hh->proto == IPAC_PROTO_IPACCESS) {
+		rc = ipa_server_conn_ccm(conn, msg);
+		if (rc < 0) {
+			/* conn is already invalid here! */
+			return -1;
+		}
+		msgb_free(msg);
+		return 0;
+	}
+
+	if (hh->proto != IPAC_PROTO_OSMO) {
+		LOGP(DLGSUP, LOGL_NOTICE, "Unsupported IPA stream ID 0x%02x\n",
+			hh->proto);
+		goto invalid;
+	}
+
+	if (!he || msgb_l2len(msg) < sizeof(*he)) {
+		LOGP(DLGSUP, LOGL_NOTICE, "short IPA message\n");
+		goto invalid;
+	}
+
+	msg->l2h = &he->data[0];
+
+	if (he->proto == IPAC_PROTO_EXT_GSUP) {
+		OSMO_ASSERT(clnt->server->read_cb != NULL);
+		clnt->server->read_cb(clnt, msg);
+		/* expecting read_cb() to free msg */
+	} else if (he->proto == IPAC_PROTO_EXT_OAP) {
+		return osmo_gsup_conn_oap_handle(clnt, msg);
+		/* osmo_gsup_client_oap_handle frees msg */
+	} else {
+		LOGP(DLGSUP, LOGL_NOTICE, "Unsupported IPA Osmo Proto 0x%02x\n",
+			hh->proto);
+		goto invalid;
+	}
+
+	return 0;
+
+invalid:
+	LOGP(DLGSUP, LOGL_NOTICE,
+	     "GSUP received an invalid IPA message from %s:%d: %s\n",
+	     conn->addr, conn->port, osmo_hexdump(msgb_l2(msg), msgb_l2len(msg)));
+	msgb_free(msg);
+	return -1;
+
+}
+
+static void osmo_tlvp_dump(const struct tlv_parsed *tlvp,
+			   int subsys, int level)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(tlvp->lv); i++) {
+		if (!TLVP_PRESENT(tlvp, i))
+			continue;
+
+		LOGP(subsys, level, "%u: %s\n", i,
+			TLVP_VAL(tlvp, i));
+		LOGP(subsys, level, "%u: %s\n", i,
+			osmo_hexdump(TLVP_VAL(tlvp, i),
+				     TLVP_LEN(tlvp, i)));
+	}
+}
+
+/* FIXME: should this be parrt of ipas_server handling, not GSUP? */
+static void tlvp_copy(void *ctx, struct tlv_parsed *out, const struct tlv_parsed *in)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(out->lv); i++) {
+		if (!TLVP_PRESENT(in, i)) {
+			if (TLVP_PRESENT(out, i)) {
+				talloc_free((void *) out->lv[i].val);
+				out->lv[i].val = NULL;
+				out->lv[i].len = 0;
+			}
+			continue;
+		}
+		out->lv[i].val = talloc_memdup(ctx, in->lv[i].val, in->lv[i].len);
+		out->lv[i].len = in->lv[i].len;
+	}
+}
+
+int osmo_gsup_conn_ccm_get(const struct osmo_gsup_conn *clnt, uint8_t **addr,
+			   uint8_t tag)
+{
+	if (!TLVP_PRESENT(&clnt->ccm, tag))
+		return -ENODEV;
+	*addr = (uint8_t *) TLVP_VAL(&clnt->ccm, tag);
+
+	return TLVP_LEN(&clnt->ccm, tag);
+}
+
+static int osmo_gsup_server_ccm_cb(struct ipa_server_conn *conn,
+				   struct msgb *msg, struct tlv_parsed *tlvp,
+				   struct ipaccess_unit *unit)
+{
+	struct osmo_gsup_conn *clnt = (struct osmo_gsup_conn *)conn->data;
+	uint8_t *addr = NULL;
+	size_t addr_len;
+
+	LOGP(DLGSUP, LOGL_INFO, "CCM Callback\n");
+
+	/* FIXME: should this be parrt of ipas_server handling, not
+	 * GSUP? */
+	tlvp_copy(clnt, &clnt->ccm, tlvp);
+	osmo_tlvp_dump(tlvp, DLGSUP, LOGL_INFO);
+
+	addr_len = osmo_gsup_conn_ccm_get(clnt, &addr, IPAC_IDTAG_SERNR);
+	if (addr_len <= 0) {
+		LOGP(DLGSUP, LOGL_ERROR, "GSUP client %s:%u has no %s IE and"
+		     " cannot be routed\n",
+		     conn->addr, conn->port,
+		     ipa_ccm_idtag_name(IPAC_IDTAG_SERNR));
+		return -EINVAL;
+	}
+
+	gsup_route_add(clnt, addr, addr_len);
+	return 0;
+}
+
+static int osmo_gsup_server_closed_cb(struct ipa_server_conn *conn)
+{
+	struct osmo_gsup_conn *clnt = (struct osmo_gsup_conn *)conn->data;
+
+	LOGP(DLGSUP, LOGL_INFO, "Lost GSUP client %s:%d\n",
+		conn->addr, conn->port);
+
+	gsup_route_del_conn(clnt);
+	llist_del(&clnt->list);
+	talloc_free(clnt);
+
+	return 0;
+}
+
+/* Add conn to the clients list in a way that conn->auc_3g_ind takes the lowest
+ * unused integer and the list of clients remains sorted by auc_3g_ind.
+ * Keep this function non-static to allow linking in a unit test. */
+void osmo_gsup_server_add_conn(struct llist_head *clients,
+			       struct osmo_gsup_conn *conn)
+{
+	struct osmo_gsup_conn *c;
+	struct osmo_gsup_conn *prev_conn;
+
+	c = llist_first_entry_or_null(clients, struct osmo_gsup_conn, list);
+
+	/* Is the first index, 0, unused? */
+	if (!c || c->auc_3g_ind > 0) {
+		conn->auc_3g_ind = 0;
+		llist_add(&conn->list, clients);
+		return;
+	}
+
+	/* Look for a gap later on */
+	prev_conn = NULL;
+	llist_for_each_entry(c, clients, list) {
+		/* skip first item, we know it has auc_3g_ind == 0. */
+		if (!prev_conn) {
+			prev_conn = c;
+			continue;
+		}
+		if (c->auc_3g_ind > prev_conn->auc_3g_ind + 1)
+			break;
+		prev_conn = c;
+	}
+
+	OSMO_ASSERT(prev_conn);
+
+	conn->auc_3g_ind = prev_conn->auc_3g_ind + 1;
+	llist_add(&conn->list, &prev_conn->list);
+}
+
+/* a client has connected to the server socket and we have accept()ed it */
+static int osmo_gsup_server_accept_cb(struct ipa_server_link *link, int fd)
+{
+	struct osmo_gsup_conn *conn;
+	struct osmo_gsup_server *gsups =
+		(struct osmo_gsup_server *) link->data;
+	int rc;
+
+	conn = talloc_zero(gsups, struct osmo_gsup_conn);
+	OSMO_ASSERT(conn);
+
+	conn->conn = ipa_server_conn_create(gsups, link, fd,
+					   osmo_gsup_server_read_cb,
+					   osmo_gsup_server_closed_cb, conn);
+	OSMO_ASSERT(conn->conn);
+	conn->conn->ccm_cb = osmo_gsup_server_ccm_cb;
+
+	/* link data structure with server structure */
+	conn->server = gsups;
+	osmo_gsup_server_add_conn(&gsups->clients, conn);
+
+	LOGP(DLGSUP, LOGL_INFO, "New GSUP client %s:%d (IND=%u)\n",
+	     conn->conn->addr, conn->conn->port, conn->auc_3g_ind);
+
+	/* request the identity of the client */
+	rc = ipa_ccm_send_id_req(fd);
+	if (rc < 0)
+		goto failed;
+#if 0
+	rc = oap_init(&gsups->oap_config, &conn->oap_state);
+	if (rc != 0)
+		goto failed;
+#endif
+	return 0;
+failed:
+	ipa_server_conn_destroy(conn->conn);
+	return -1;
+}
+
+struct osmo_gsup_server *
+osmo_gsup_server_create(void *ctx, const char *ip_addr, uint16_t tcp_port,
+			osmo_gsup_read_cb_t read_cb,
+			struct llist_head *lu_op_lst, void *priv)
+{
+	struct osmo_gsup_server *gsups;
+	int rc;
+
+	gsups = talloc_zero(ctx, struct osmo_gsup_server);
+	OSMO_ASSERT(gsups);
+
+	INIT_LLIST_HEAD(&gsups->clients);
+	INIT_LLIST_HEAD(&gsups->routes);
+
+	gsups->link = ipa_server_link_create(gsups,
+					/* no e1inp */ NULL,
+					ip_addr, tcp_port,
+					osmo_gsup_server_accept_cb,
+					gsups);
+	if (!gsups->link)
+		goto failed;
+
+	gsups->read_cb = read_cb;
+	gsups->priv = priv;
+
+	rc = ipa_server_link_open(gsups->link);
+	if (rc < 0)
+		goto failed;
+
+	gsups->luop = lu_op_lst;
+
+	return gsups;
+
+failed:
+	osmo_gsup_server_destroy(gsups);
+	return NULL;
+}
+
+void osmo_gsup_server_destroy(struct osmo_gsup_server *gsups)
+{
+	if (gsups->link) {
+		ipa_server_link_close(gsups->link);
+		ipa_server_link_destroy(gsups->link);
+		gsups->link = NULL;
+	}
+	talloc_free(gsups);
+}
+
+/* Set GSUP message's pdp_infos[0] to a wildcard APN.
+ * Use the provided apn_buf to store the produced APN data. This must remain valid until
+ * osmo_gsup_encode() is done. Return 0 if an entry was added, -ENOMEM if the provided buffer is too
+ * small. */
+int osmo_gsup_configure_wildcard_apn(struct osmo_gsup_message *gsup,
+				     uint8_t *apn_buf, size_t apn_buf_size)
+{
+	int l;
+
+	l = osmo_apn_from_str(apn_buf, apn_buf_size, "*");
+	if (l <= 0)
+		return -ENOMEM;
+
+	gsup->pdp_infos[0].apn_enc = apn_buf;
+	gsup->pdp_infos[0].apn_enc_len = l;
+	gsup->pdp_infos[0].have_info = 1;
+	gsup->num_pdp_infos = 1;
+	/* FIXME: use real value: */
+	gsup->pdp_infos[0].context_id = 1;
+
+	return 0;
+}
+
+/**
+ * Populate a gsup message structure with an Insert Subscriber Data Message.
+ * All required memory buffers for data pointed to by pointers in struct omso_gsup_message
+ * must be allocated by the caller and should have the same lifetime as the gsup parameter.
+ *
+ * \param[out] gsup  The gsup message to populate.
+ * \param[in] imsi  The subscriber's IMSI.
+ * \param[in] msisdn The subscriber's MSISDN.
+ * \param[out] msisdn_enc A buffer large enough to store the MSISDN in encoded form.
+ * \param[in] msisdn_enc_size Size of the buffer (must be >= OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN).
+ * \param[out] apn_buf A buffer large enough to store an APN (required if cn_domain is OSMO_GSUP_CN_DOMAIN_PS).
+ * \param[in] apn_buf_size Size of APN buffer (must be >= APN_MAXLEN).
+ * \param[in] cn_domain The CN Domain of the subscriber connection.
+ * \returns 0 on success, and negative on error.
+ */
+int osmo_gsup_create_insert_subscriber_data_msg(struct osmo_gsup_message *gsup, const char *imsi, const char *msisdn,
+						uint8_t *msisdn_enc, size_t msisdn_enc_size,
+						uint8_t *apn_buf, size_t apn_buf_size,
+						enum osmo_gsup_cn_domain cn_domain)
+{
+	int len;
+
+	OSMO_ASSERT(gsup);
+
+	gsup->message_type = OSMO_GSUP_MSGT_INSERT_DATA_REQUEST;
+	osmo_strlcpy(gsup->imsi, imsi, sizeof(gsup->imsi));
+
+	if (msisdn_enc_size < OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN)
+		return -ENOSPC;
+
+	OSMO_ASSERT(msisdn_enc);
+	len = gsm48_encode_bcd_number(msisdn_enc, msisdn_enc_size, 0, msisdn);
+	if (len < 1) {
+		LOGP(DLGSUP, LOGL_ERROR, "%s: Error: cannot encode MSISDN '%s'\n", imsi, msisdn);
+		return -ENOSPC;
+	}
+	gsup->msisdn_enc = msisdn_enc;
+	gsup->msisdn_enc_len = len;
+
+	#pragma message "FIXME: deal with encoding the following data: gsup.hlr_enc"
+
+	gsup->cn_domain = cn_domain;
+	if (gsup->cn_domain == OSMO_GSUP_CN_DOMAIN_PS) {
+		OSMO_ASSERT(apn_buf_size >= APN_MAXLEN);
+		OSMO_ASSERT(apn_buf);
+		/* FIXME: PDP infos - use more fine-grained access control
+		   instead of wildcard APN */
+		osmo_gsup_configure_wildcard_apn(gsup, apn_buf, apn_buf_size);
+	}
+
+	return 0;
+}
diff --git a/src/gsup_server.h b/src/gsup_server.h
new file mode 100644
index 0000000..9c4d483
--- /dev/null
+++ b/src/gsup_server.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/abis/ipa.h>
+#include <osmocom/abis/ipaccess.h>
+#include <osmocom/gsm/gsup.h>
+
+#ifndef OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN
+#define OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN	43 /* TS 24.008 10.5.4.7 */
+#endif
+
+struct osmo_gsup_conn;
+
+/* Expects message in msg->l2h */
+typedef int (*osmo_gsup_read_cb_t)(struct osmo_gsup_conn *conn, struct msgb *msg);
+
+struct osmo_gsup_server {
+	/* private data of the application/user */
+	void *priv;
+
+	/* list of osmo_gsup_conn */
+	struct llist_head clients;
+
+	/* lu_operations list */
+	struct llist_head *luop;
+
+	struct ipa_server_link *link;
+	osmo_gsup_read_cb_t read_cb;
+	struct llist_head routes;
+};
+
+
+/* a single connection to a given client (SGSN, MSC) */
+struct osmo_gsup_conn {
+	struct llist_head list;
+
+	struct osmo_gsup_server *server;
+	struct ipa_server_conn *conn;
+	//struct oap_state oap_state;
+	struct tlv_parsed ccm;
+
+	unsigned int auc_3g_ind; /*!< IND index used for UMTS AKA SQN */
+
+	/* Set when Location Update is received: */
+	bool supports_cs; /* client supports OSMO_GSUP_CN_DOMAIN_CS */
+	bool supports_ps; /* client supports OSMO_GSUP_CN_DOMAIN_PS */
+};
+
+
+int osmo_gsup_conn_send(struct osmo_gsup_conn *conn, struct msgb *msg);
+int osmo_gsup_conn_ccm_get(const struct osmo_gsup_conn *clnt, uint8_t **addr,
+			   uint8_t tag);
+
+struct osmo_gsup_server *osmo_gsup_server_create(void *ctx,
+						 const char *ip_addr,
+						 uint16_t tcp_port,
+						 osmo_gsup_read_cb_t read_cb,
+						 struct llist_head *lu_op_lst,
+						 void *priv);
+
+void osmo_gsup_server_destroy(struct osmo_gsup_server *gsups);
+
+int osmo_gsup_configure_wildcard_apn(struct osmo_gsup_message *gsup,
+				     uint8_t *apn_buf, size_t apn_buf_size);
+int osmo_gsup_create_insert_subscriber_data_msg(struct osmo_gsup_message *gsup, const char *imsi, const char *msisdn,
+					    uint8_t *msisdn_enc, size_t msisdn_enc_size,
+				            uint8_t *apn_buf, size_t apn_buf_size,
+					    enum osmo_gsup_cn_domain cn_domain);
diff --git a/src/gsupclient/Makefile.am b/src/gsupclient/Makefile.am
new file mode 100644
index 0000000..4a449ec
--- /dev/null
+++ b/src/gsupclient/Makefile.am
@@ -0,0 +1,20 @@
+# This is _NOT_ the library release version, it's an API version.
+# Please read chapter "Library interface versions" of the libtool documentation
+# before making any modifications: https://www.gnu.org/software/libtool/manual/html_node/Versioning.html
+LIBVERSION=0:0:0
+
+AM_CFLAGS = -Wall $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)/include \
+	    $(TALLOC_CFLAGS) $(LIBOSMOCORE_CFLAGS) $(LIBOSMOABIS_CFLAGS)
+
+lib_LTLIBRARIES = libosmo-gsup-client.la
+
+libosmo_gsup_client_la_SOURCES = gsup_client.c
+
+libosmo_gsup_client_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined
+libosmo_gsup_client_la_LIBADD = $(TALLOC_LIBS) $(LIBOSMOCORE_LIBS) $(LIBOSMOABIS_LIBS)
+
+noinst_PROGRAMS = gsup-test-client
+
+gsup_test_client_SOURCES = gsup_test_client.c
+gsup_test_client_LDADD = $(TALLOC_LIBS) $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) \
+			 libosmo-gsup-client.la
diff --git a/src/gsupclient/gsup_client.c b/src/gsupclient/gsup_client.c
new file mode 100644
index 0000000..d34a22d
--- /dev/null
+++ b/src/gsupclient/gsup_client.c
@@ -0,0 +1,345 @@
+/* Generic Subscriber Update Protocol client */
+
+/* (C) 2014-2016 by Sysmocom s.f.m.c. GmbH
+ * All Rights Reserved
+ *
+ * Author: Jacob Erlbeck
+ * Author: Neels Hofmeyr
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/gsupclient/gsup_client.h>
+
+#include <osmocom/abis/ipa.h>
+#include <osmocom/gsm/oap_client.h>
+#include <osmocom/gsm/protocol/ipaccess.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+
+#include <errno.h>
+#include <string.h>
+
+static void start_test_procedure(struct osmo_gsup_client *gsupc);
+
+static void gsup_client_send_ping(struct osmo_gsup_client *gsupc)
+{
+	struct msgb *msg = osmo_gsup_client_msgb_alloc();
+
+	msg->l2h = msgb_put(msg, 1);
+	msg->l2h[0] = IPAC_MSGT_PING;
+	ipa_msg_push_header(msg, IPAC_PROTO_IPACCESS);
+	ipa_client_conn_send(gsupc->link, msg);
+}
+
+static int gsup_client_connect(struct osmo_gsup_client *gsupc)
+{
+	int rc;
+
+	if (gsupc->is_connected)
+		return 0;
+
+	if (osmo_timer_pending(&gsupc->connect_timer)) {
+		LOGP(DLGSUP, LOGL_DEBUG,
+		     "GSUP connect: connect timer already running\n");
+		osmo_timer_del(&gsupc->connect_timer);
+	}
+
+	if (osmo_timer_pending(&gsupc->ping_timer)) {
+		LOGP(DLGSUP, LOGL_DEBUG,
+		     "GSUP connect: ping timer already running\n");
+		osmo_timer_del(&gsupc->ping_timer);
+	}
+
+	if (ipa_client_conn_clear_queue(gsupc->link) > 0)
+		LOGP(DLGSUP, LOGL_DEBUG, "GSUP connect: discarded stored messages\n");
+
+	rc = ipa_client_conn_open(gsupc->link);
+
+	if (rc >= 0) {
+		LOGP(DLGSUP, LOGL_NOTICE, "GSUP connecting to %s:%d\n",
+		     gsupc->link->addr, gsupc->link->port);
+		return 0;
+	}
+
+	LOGP(DLGSUP, LOGL_ERROR, "GSUP failed to connect to %s:%d: %s\n",
+	     gsupc->link->addr, gsupc->link->port, strerror(-rc));
+
+	if (rc == -EBADF || rc == -ENOTSOCK || rc == -EAFNOSUPPORT ||
+	    rc == -EINVAL)
+		return rc;
+
+	osmo_timer_schedule(&gsupc->connect_timer,
+			    OSMO_GSUP_CLIENT_RECONNECT_INTERVAL, 0);
+
+	LOGP(DLGSUP, LOGL_INFO, "Scheduled timer to retry GSUP connect to %s:%d\n",
+	     gsupc->link->addr, gsupc->link->port);
+
+	return 0;
+}
+
+static void connect_timer_cb(void *gsupc_)
+{
+	struct osmo_gsup_client *gsupc = gsupc_;
+
+	if (gsupc->is_connected)
+		return;
+
+	gsup_client_connect(gsupc);
+}
+
+static void client_send(struct osmo_gsup_client *gsupc, int proto_ext,
+			struct msgb *msg_tx)
+{
+	ipa_prepend_header_ext(msg_tx, proto_ext);
+	ipa_msg_push_header(msg_tx, IPAC_PROTO_OSMO);
+	ipa_client_conn_send(gsupc->link, msg_tx);
+	/* msg_tx is now queued and will be freed. */
+}
+
+static void gsup_client_oap_register(struct osmo_gsup_client *gsupc)
+{
+	struct msgb *msg_tx;
+	int rc;
+	rc = osmo_oap_client_register(&gsupc->oap_state, &msg_tx);
+
+	if ((rc < 0) || (!msg_tx)) {
+		LOGP(DLGSUP, LOGL_ERROR, "GSUP OAP set up, but cannot register.\n");
+		return;
+	}
+
+	client_send(gsupc, IPAC_PROTO_EXT_OAP, msg_tx);
+}
+
+static void gsup_client_updown_cb(struct ipa_client_conn *link, int up)
+{
+	struct osmo_gsup_client *gsupc = link->data;
+
+	LOGP(DLGSUP, LOGL_INFO, "GSUP link to %s:%d %s\n",
+		     link->addr, link->port, up ? "UP" : "DOWN");
+
+	gsupc->is_connected = up;
+
+	if (up) {
+		start_test_procedure(gsupc);
+
+		if (gsupc->oap_state.state == OSMO_OAP_INITIALIZED)
+			gsup_client_oap_register(gsupc);
+
+		osmo_timer_del(&gsupc->connect_timer);
+	} else {
+		osmo_timer_del(&gsupc->ping_timer);
+
+		osmo_timer_schedule(&gsupc->connect_timer,
+				    OSMO_GSUP_CLIENT_RECONNECT_INTERVAL, 0);
+	}
+}
+
+static int gsup_client_oap_handle(struct osmo_gsup_client *gsupc, struct msgb *msg_rx)
+{
+	int rc;
+	struct msgb *msg_tx;
+
+	/* If the oap_state is disabled, this will reject the messages. */
+	rc = osmo_oap_client_handle(&gsupc->oap_state, msg_rx, &msg_tx);
+	msgb_free(msg_rx);
+	if (rc < 0)
+		return rc;
+
+	if (msg_tx)
+		client_send(gsupc, IPAC_PROTO_EXT_OAP, msg_tx);
+
+	return 0;
+}
+
+static int gsup_client_read_cb(struct ipa_client_conn *link, struct msgb *msg)
+{
+	struct ipaccess_head *hh = (struct ipaccess_head *) msg->data;
+	struct ipaccess_head_ext *he = (struct ipaccess_head_ext *) msgb_l2(msg);
+	struct osmo_gsup_client *gsupc = (struct osmo_gsup_client *)link->data;
+	int rc;
+	struct ipaccess_unit ipa_dev = {
+		/* see gsup_client_create() on const vs non-const */
+		.unit_name = (char*)gsupc->unit_name,
+	};
+
+	OSMO_ASSERT(ipa_dev.unit_name);
+
+	msg->l2h = &hh->data[0];
+
+	rc = ipaccess_bts_handle_ccm(link, &ipa_dev, msg);
+
+	if (rc < 0) {
+		LOGP(DLGSUP, LOGL_NOTICE,
+		     "GSUP received an invalid IPA/CCM message from %s:%d\n",
+		     link->addr, link->port);
+		/* Link has been closed */
+		gsupc->is_connected = 0;
+		msgb_free(msg);
+		return -1;
+	}
+
+	if (rc == 1) {
+		uint8_t msg_type = *(msg->l2h);
+		/* CCM message */
+		if (msg_type == IPAC_MSGT_PONG) {
+			LOGP(DLGSUP, LOGL_DEBUG, "GSUP receiving PONG\n");
+			gsupc->got_ipa_pong = 1;
+		}
+
+		msgb_free(msg);
+		return 0;
+	}
+
+	if (hh->proto != IPAC_PROTO_OSMO)
+		goto invalid;
+
+	if (!he || msgb_l2len(msg) < sizeof(*he))
+		goto invalid;
+
+	msg->l2h = &he->data[0];
+
+	if (he->proto == IPAC_PROTO_EXT_GSUP) {
+		OSMO_ASSERT(gsupc->read_cb != NULL);
+		gsupc->read_cb(gsupc, msg);
+		/* expecting read_cb() to free msg */
+	} else if (he->proto == IPAC_PROTO_EXT_OAP) {
+		return gsup_client_oap_handle(gsupc, msg);
+		/* gsup_client_oap_handle frees msg */
+	} else
+		goto invalid;
+
+	return 0;
+
+invalid:
+	LOGP(DLGSUP, LOGL_NOTICE,
+	     "GSUP received an invalid IPA message from %s:%d, size = %d\n",
+	     link->addr, link->port, msgb_length(msg));
+
+	msgb_free(msg);
+	return -1;
+}
+
+static void ping_timer_cb(void *gsupc_)
+{
+	struct osmo_gsup_client *gsupc = gsupc_;
+
+	LOGP(DLGSUP, LOGL_INFO, "GSUP ping callback (%s, %s PONG)\n",
+	     gsupc->is_connected ? "connected" : "not connected",
+	     gsupc->got_ipa_pong ? "got" : "didn't get");
+
+	if (gsupc->got_ipa_pong) {
+		start_test_procedure(gsupc);
+		return;
+	}
+
+	LOGP(DLGSUP, LOGL_NOTICE, "GSUP ping timed out, reconnecting\n");
+	ipa_client_conn_close(gsupc->link);
+	gsupc->is_connected = 0;
+
+	gsup_client_connect(gsupc);
+}
+
+static void start_test_procedure(struct osmo_gsup_client *gsupc)
+{
+	osmo_timer_setup(&gsupc->ping_timer, ping_timer_cb, gsupc);
+
+	gsupc->got_ipa_pong = 0;
+	osmo_timer_schedule(&gsupc->ping_timer, OSMO_GSUP_CLIENT_PING_INTERVAL, 0);
+	LOGP(DLGSUP, LOGL_DEBUG, "GSUP sending PING\n");
+	gsup_client_send_ping(gsupc);
+}
+
+struct osmo_gsup_client *osmo_gsup_client_create(void *talloc_ctx,
+						 const char *unit_name,
+						 const char *ip_addr,
+						 unsigned int tcp_port,
+						 osmo_gsup_client_read_cb_t read_cb,
+						 struct osmo_oap_client_config *oapc_config)
+{
+	struct osmo_gsup_client *gsupc;
+	int rc;
+
+	gsupc = talloc_zero(talloc_ctx, struct osmo_gsup_client);
+	OSMO_ASSERT(gsupc);
+
+	/* struct ipaccess_unit has a non-const unit_name, so let's copy to be
+	 * able to have a non-const unit_name here as well. To not taint the
+	 * public gsup_client API, let's store it in a const char* anyway. */
+	gsupc->unit_name = talloc_strdup(gsupc, unit_name);
+	OSMO_ASSERT(gsupc->unit_name);
+
+	/* a NULL oapc_config will mark oap_state disabled. */
+	rc = osmo_oap_client_init(oapc_config, &gsupc->oap_state);
+	if (rc != 0)
+		goto failed;
+
+	gsupc->link = ipa_client_conn_create(gsupc,
+					     /* no e1inp */ NULL,
+					     0,
+					     ip_addr, tcp_port,
+					     gsup_client_updown_cb,
+					     gsup_client_read_cb,
+					     /* default write_cb */ NULL,
+					     gsupc);
+	if (!gsupc->link)
+		goto failed;
+
+	osmo_timer_setup(&gsupc->connect_timer, connect_timer_cb, gsupc);
+
+	rc = gsup_client_connect(gsupc);
+
+	if (rc < 0)
+		goto failed;
+
+	gsupc->read_cb = read_cb;
+
+	return gsupc;
+
+failed:
+	osmo_gsup_client_destroy(gsupc);
+	return NULL;
+}
+
+void osmo_gsup_client_destroy(struct osmo_gsup_client *gsupc)
+{
+	osmo_timer_del(&gsupc->connect_timer);
+	osmo_timer_del(&gsupc->ping_timer);
+
+	if (gsupc->link) {
+		ipa_client_conn_close(gsupc->link);
+		ipa_client_conn_destroy(gsupc->link);
+		gsupc->link = NULL;
+	}
+	talloc_free(gsupc);
+}
+
+int osmo_gsup_client_send(struct osmo_gsup_client *gsupc, struct msgb *msg)
+{
+	if (!gsupc || !gsupc->is_connected) {
+		LOGP(DLGSUP, LOGL_ERROR, "GSUP not connected, unable to send %s\n", msgb_hexdump(msg));
+		msgb_free(msg);
+		return -ENOTCONN;
+	}
+
+	client_send(gsupc, IPAC_PROTO_EXT_GSUP, msg);
+
+	return 0;
+}
+
+struct msgb *osmo_gsup_client_msgb_alloc(void)
+{
+	return msgb_alloc_headroom(4000, 64, __func__);
+}
diff --git a/src/gsupclient/gsup_test_client.c b/src/gsupclient/gsup_test_client.c
new file mode 100644
index 0000000..b0362ad
--- /dev/null
+++ b/src/gsupclient/gsup_test_client.c
@@ -0,0 +1,321 @@
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/application.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/gsm/gsup.h>
+
+#include <osmocom/gsupclient/gsup_client.h>
+
+static struct osmo_gsup_client *g_gc;
+
+
+/***********************************************************************
+ * IMSI Operation
+ ***********************************************************************/
+static LLIST_HEAD(g_imsi_ops);
+
+struct imsi_op_stats {
+	uint32_t num_alloc;
+	uint32_t num_released;
+	uint32_t num_rx_success;
+	uint32_t num_rx_error;
+	uint32_t num_timeout;
+};
+
+enum imsi_op_type {
+	IMSI_OP_SAI,
+	IMSI_OP_LU,
+	IMSI_OP_ISD,
+	_NUM_IMSI_OP
+};
+
+static const struct value_string imsi_op_names[] = {
+	{ IMSI_OP_SAI, "SAI" },
+	{ IMSI_OP_LU, "LU" },
+	{ IMSI_OP_ISD, "ISD" },
+	{ 0, NULL }
+};
+
+static struct imsi_op_stats imsi_op_stats[_NUM_IMSI_OP];
+
+struct imsi_op {
+	struct llist_head list;
+	char imsi[17];
+	enum imsi_op_type type;
+	struct osmo_timer_list timer;
+};
+
+static struct imsi_op *imsi_op_find(const char *imsi,
+			     enum imsi_op_type type)
+{
+	struct imsi_op *io;
+
+	llist_for_each_entry(io, &g_imsi_ops, list) {
+		if (!strcmp(io->imsi, imsi) && io->type == type)
+			return io;
+	}
+	return NULL;
+}
+
+static void imsi_op_timer_cb(void *data);
+
+static struct imsi_op *imsi_op_alloc(void *ctx, const char *imsi,
+				enum imsi_op_type type)
+{
+	struct imsi_op *io;
+
+	if (imsi_op_find(imsi, type))
+		return NULL;
+
+	io = talloc_zero(ctx, struct imsi_op);
+	OSMO_STRLCPY_ARRAY(io->imsi, imsi);
+	io->type = type;
+	osmo_timer_setup(&io->timer, imsi_op_timer_cb, io);
+	llist_add(&io->list, &g_imsi_ops);
+	imsi_op_stats[type].num_alloc++;
+
+	return io;
+}
+
+static void imsi_op_release(struct imsi_op *io)
+{
+	osmo_timer_del(&io->timer);
+	llist_del(&io->list);
+	imsi_op_stats[io->type].num_released++;
+	talloc_free(io);
+}
+
+static void imsi_op_timer_cb(void *data)
+{
+	struct imsi_op *io = data;
+	printf("%s: Timer expiration\n", io->imsi);
+	imsi_op_stats[io->type].num_timeout++;
+	imsi_op_release(io);
+}
+
+/* allocate + generate + send Send-Auth-Info */
+static int req_auth_info(const char *imsi)
+{
+	struct imsi_op *io = imsi_op_alloc(g_gc, imsi, IMSI_OP_SAI);
+	struct osmo_gsup_message gsup = {0};
+	struct msgb *msg = msgb_alloc_headroom(1200, 200, __func__);
+	int rc;
+
+	OSMO_STRLCPY_ARRAY(gsup.imsi, io->imsi);
+	gsup.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_REQUEST;
+
+	rc = osmo_gsup_encode(msg, &gsup);
+	if (rc < 0) {
+		printf("%s: encoding failure (%s)\n", imsi, strerror(-rc));
+		return rc;
+	}
+
+	return osmo_gsup_client_send(g_gc, msg);
+}
+
+/* allocate + generate + send Send-Auth-Info */
+static int req_loc_upd(const char *imsi)
+{
+	struct imsi_op *io = imsi_op_alloc(g_gc, imsi, IMSI_OP_LU);
+	struct osmo_gsup_message gsup = {0};
+	struct msgb *msg = msgb_alloc_headroom(1200, 200, __func__);
+	int rc;
+
+	OSMO_STRLCPY_ARRAY(gsup.imsi, io->imsi);
+	gsup.message_type = OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST;
+
+	rc = osmo_gsup_encode(msg, &gsup);
+	if (rc < 0) {
+		printf("%s: encoding failure (%s)\n", imsi, strerror(-rc));
+		return rc;
+	}
+
+	return osmo_gsup_client_send(g_gc, msg);
+}
+
+static int resp_isd(struct imsi_op *io)
+{
+	struct osmo_gsup_message gsup = {0};
+	struct msgb *msg = msgb_alloc_headroom(1200, 200, __func__);
+	int rc;
+
+	OSMO_STRLCPY_ARRAY(gsup.imsi, io->imsi);
+	gsup.message_type = OSMO_GSUP_MSGT_INSERT_DATA_RESULT;
+
+	rc = osmo_gsup_encode(msg, &gsup);
+	if (rc < 0) {
+		printf("%s: encoding failure (%s)\n", io->imsi, strerror(-rc));
+		return rc;
+	}
+
+	imsi_op_release(io);
+
+	return osmo_gsup_client_send(g_gc, msg);
+}
+
+/* receive an incoming GSUP message */
+static void imsi_op_rx_gsup(struct imsi_op *io, const struct osmo_gsup_message *gsup)
+{
+	int is_error = 0, rc;
+
+	if (OSMO_GSUP_IS_MSGT_ERROR(gsup->message_type)) {
+		imsi_op_stats[io->type].num_rx_error++;
+		is_error = 1;
+	} else
+		imsi_op_stats[io->type].num_rx_success++;
+
+	switch (io->type) {
+	case IMSI_OP_SAI:
+		printf("%s; SAI Response%s\n", io->imsi, is_error ? ": ERROR" : "");
+		/* now that we have auth tuples, request LU */
+		rc = req_loc_upd(io->imsi);
+		if (rc < 0)
+			printf("Failed to request Location Update for %s\n", io->imsi);
+		imsi_op_release(io);
+		break;
+	case IMSI_OP_LU:
+		printf("%s; LU Response%s\n", io->imsi, is_error ? ": ERROR" : "");
+		imsi_op_release(io);
+		break;
+	case IMSI_OP_ISD:
+		printf("%s; ISD Request%s\n", io->imsi, is_error ? ": ERROR" : "");
+		rc = resp_isd(io);
+		if (rc < 0)
+			printf("Failed to insert subscriber data for %s\n", io->imsi);
+		break;
+	default:
+		printf("%s: Unknown\n", io->imsi);
+		imsi_op_release(io);
+		break;
+	}
+}
+
+static int op_type_by_gsup_msgt(enum osmo_gsup_message_type msg_type)
+{
+	switch (msg_type) {
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_RESULT:
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_ERROR:
+		return IMSI_OP_SAI;
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT:
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR:
+		return IMSI_OP_LU;
+	case OSMO_GSUP_MSGT_INSERT_DATA_REQUEST:
+		return IMSI_OP_ISD;
+	default:
+		printf("Unknown GSUP msg_type %u\n", msg_type);
+		return -1;
+	}
+}
+
+static int gsupc_read_cb(struct osmo_gsup_client *gsupc, struct msgb *msg)
+{
+	struct osmo_gsup_message gsup_msg = {0};
+	struct imsi_op *io = NULL;
+	int rc;
+
+	DEBUGP(DLGSUP, "Rx GSUP %s\n", msgb_hexdump(msg));
+
+	rc = osmo_gsup_decode(msgb_l2(msg), msgb_l2len(msg), &gsup_msg);
+	if (rc < 0)
+		return rc;
+
+	if (!gsup_msg.imsi[0])
+		return -1;
+
+	rc = op_type_by_gsup_msgt(gsup_msg.message_type);
+	if (rc < 0)
+		return rc;
+
+	switch (rc) {
+	case IMSI_OP_SAI:
+	case IMSI_OP_LU:
+		io = imsi_op_find(gsup_msg.imsi, rc);
+		break;
+	case IMSI_OP_ISD:
+		/* ISD is an inbound transaction */
+		io = imsi_op_alloc(g_gc, gsup_msg.imsi, IMSI_OP_ISD);
+		break;
+	}
+	if (!io)
+		return -1;
+
+	imsi_op_rx_gsup(io, &gsup_msg);
+	msgb_free(msg);
+
+	return 0;
+}
+
+static void print_report(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(imsi_op_stats); i++) {
+		struct imsi_op_stats *st = &imsi_op_stats[i];
+		const char *name = get_value_string(imsi_op_names, i);
+		printf("%s: %u alloc, %u released, %u success, %u error , %u tout\n",
+			name, st->num_alloc, st->num_released, st->num_rx_success,
+			st->num_rx_error, st->num_timeout);
+	}
+}
+
+static void sig_cb(int sig)
+{
+	switch (sig) {
+	case SIGINT:
+		print_report();
+		exit(0);
+		break;
+	}
+}
+
+/* default categories */
+static struct log_info_cat default_categories[] = {
+};
+
+static const struct log_info gsup_test_client_log_info = {
+	.cat = default_categories,
+	.num_cat = ARRAY_SIZE(default_categories),
+};
+
+int main(int argc, char **argv)
+{
+	unsigned long long i;
+	char *server_host = "127.0.0.1";
+	uint16_t server_port = OSMO_GSUP_PORT;
+	void *ctx = talloc_named_const(NULL, 0, "gsup_test_client");
+
+	osmo_init_logging2(ctx, &gsup_test_client_log_info);
+
+	g_gc = osmo_gsup_client_create(ctx, "GSUPTEST", server_host, server_port,
+					gsupc_read_cb, NULL);
+
+
+	signal(SIGINT, sig_cb);
+
+	for (i = 0; i < 10000; i++) {
+		unsigned long long imsi = 901790000000000 + i;
+		char imsi_buf[17] = { 0 };
+		int rc;
+
+		snprintf(imsi_buf, sizeof(imsi_buf), "%015llu", imsi);
+		rc = req_auth_info(imsi_buf);
+		if (rc < 0)
+			printf("Failed to request Auth Info for %s\n", imsi_buf);
+
+		osmo_select_main(0);
+	}
+
+	while (1) {
+		osmo_select_main(0);
+	}
+
+	print_report();
+	exit(0);
+}
diff --git a/src/hlr.c b/src/hlr.c
new file mode 100644
index 0000000..78d6c91
--- /dev/null
+++ b/src/hlr.c
@@ -0,0 +1,693 @@
+/* (C) 2016 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 <signal.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <getopt.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/telnet_interface.h>
+#include <osmocom/vty/ports.h>
+#include <osmocom/ctrl/control_vty.h>
+#include <osmocom/gsm/apn.h>
+
+#include "db.h"
+#include "hlr.h"
+#include "ctrl.h"
+#include "logging.h"
+#include "gsup_server.h"
+#include "gsup_router.h"
+#include "rand.h"
+#include "luop.h"
+#include "hlr_vty.h"
+#include "hlr_ussd.h"
+
+struct hlr *g_hlr;
+static int quit = 0;
+
+/* Trigger 'Insert Subscriber Data' messages to all connected GSUP clients.
+ *
+ * \param[in] subscr  A subscriber we have new data to send for.
+ */
+void
+osmo_hlr_subscriber_update_notify(struct hlr_subscriber *subscr)
+{
+        struct osmo_gsup_conn *co;
+
+	if (g_hlr->gs == NULL) {
+		LOGP(DLGSUP, LOGL_DEBUG,
+		     "IMSI %s: NOT Notifying peers of subscriber data change,"
+		     " there is no GSUP server\n",
+		     subscr->imsi);
+		return;
+	}
+
+	llist_for_each_entry(co, &g_hlr->gs->clients, list) {
+		struct osmo_gsup_message gsup = { };
+		uint8_t msisdn_enc[OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN];
+		uint8_t apn[APN_MAXLEN];
+		struct msgb *msg_out;
+		uint8_t *peer;
+		int peer_len;
+		size_t peer_strlen;
+		const char *peer_compare;
+		enum osmo_gsup_cn_domain cn_domain;
+
+		if (co->supports_ps) {
+			cn_domain = OSMO_GSUP_CN_DOMAIN_PS;
+			peer_compare = subscr->sgsn_number;
+		} else if (co->supports_cs) {
+			cn_domain = OSMO_GSUP_CN_DOMAIN_CS;
+			peer_compare = subscr->vlr_number;
+		} else {
+			/* We have not yet received a location update from this GSUP client.*/
+			continue;
+		}
+
+		peer_len = osmo_gsup_conn_ccm_get(co, &peer, IPAC_IDTAG_SERNR);
+		if (peer_len < 0) {
+			LOGP(DLGSUP, LOGL_ERROR,
+			       "IMSI='%s': cannot get peer name for connection %s:%u\n", subscr->imsi,
+			       co && co->conn && co->conn->server? co->conn->server->addr : "unset",
+			       co && co->conn && co->conn->server? co->conn->server->port : 0);
+			continue;
+		}
+
+		peer_strlen = strnlen((const char*)peer, peer_len);
+		if (strlen(peer_compare) != peer_strlen || strncmp(peer_compare, (const char *)peer, peer_len)) {
+			/* Mismatch. The subscriber is not subscribed with this GSUP client. */
+			/* I hope peer is always nul terminated... */
+			if (peer_strlen < peer_len)
+				LOGP(DLGSUP, LOGL_DEBUG,
+				     "IMSI %s: subscriber change: skipping %s peer %s\n",
+				     subscr->imsi, cn_domain == OSMO_GSUP_CN_DOMAIN_PS ? "PS" : "CS",
+				     osmo_quote_str((char*)peer, -1));
+			continue;
+		}
+
+		LOGP(DLGSUP, LOGL_DEBUG,
+		     "IMSI %s: subscriber change: notifying %s peer %s\n",
+		     subscr->imsi, cn_domain == OSMO_GSUP_CN_DOMAIN_PS ? "PS" : "CS",
+		     osmo_quote_str(peer_compare, -1));
+
+		if (osmo_gsup_create_insert_subscriber_data_msg(&gsup, subscr->imsi, subscr->msisdn, msisdn_enc,
+								sizeof(msisdn_enc), apn, sizeof(apn), cn_domain) != 0) {
+			LOGP(DLGSUP, LOGL_ERROR,
+			       "IMSI='%s': Cannot notify GSUP client; could not create gsup message "
+			       "for %s:%u\n", subscr->imsi,
+			       co && co->conn && co->conn->server? co->conn->server->addr : "unset",
+			       co && co->conn && co->conn->server? co->conn->server->port : 0);
+			continue;
+		}
+
+		/* Send ISD to MSC/SGSN */
+		msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP ISD UPDATE");
+		if (msg_out == NULL) {
+			LOGP(DLGSUP, LOGL_ERROR,
+			       "IMSI='%s': Cannot notify GSUP client; could not allocate msg buffer "
+			       "for %s:%u\n", subscr->imsi,
+			       co && co->conn && co->conn->server? co->conn->server->addr : "unset",
+			       co && co->conn && co->conn->server? co->conn->server->port : 0);
+			continue;
+		}
+		osmo_gsup_encode(msg_out, &gsup);
+
+		if (osmo_gsup_addr_send(g_hlr->gs, peer, peer_len, msg_out) < 0) {
+			LOGP(DMAIN, LOGL_ERROR,
+			       "IMSI='%s': Cannot notify GSUP client; send operation failed "
+			       "for %s:%u\n", subscr->imsi,
+			       co && co->conn && co->conn->server? co->conn->server->addr : "unset",
+			       co && co->conn && co->conn->server? co->conn->server->port : 0);
+			continue;
+		}
+	}
+}
+
+/***********************************************************************
+ * Send Auth Info handling
+ ***********************************************************************/
+
+/* process an incoming SAI request */
+static int rx_send_auth_info(struct osmo_gsup_conn *conn,
+			     const struct osmo_gsup_message *gsup,
+			     struct db_context *dbc)
+{
+	struct osmo_gsup_message gsup_out;
+	struct msgb *msg_out;
+	int rc;
+
+	/* initialize return message structure */
+	memset(&gsup_out, 0, sizeof(gsup_out));
+	memcpy(&gsup_out.imsi, &gsup->imsi, sizeof(gsup_out.imsi));
+
+	rc = db_get_auc(dbc, gsup->imsi, conn->auc_3g_ind,
+			gsup_out.auth_vectors,
+			ARRAY_SIZE(gsup_out.auth_vectors),
+			gsup->rand, gsup->auts);
+	if (rc <= 0) {
+		gsup_out.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_ERROR;
+		switch (rc) {
+		case 0:
+			/* 0 means "0 tuples generated", which shouldn't happen.
+			 * Treat the same as "no auth data". */
+		case -ENOKEY:
+			LOGP(DAUC, LOGL_NOTICE, "%s: IMSI known, but has no auth data;"
+			     " Returning slightly inaccurate cause 'IMSI Unknown' via GSUP\n",
+			     gsup->imsi);
+			gsup_out.cause = GMM_CAUSE_IMSI_UNKNOWN;
+			break;
+		case -ENOENT:
+			LOGP(DAUC, LOGL_NOTICE, "%s: IMSI not known\n", gsup->imsi);
+			gsup_out.cause = GMM_CAUSE_IMSI_UNKNOWN;
+			break;
+		default:
+			LOGP(DAUC, LOGL_ERROR, "%s: failure to look up IMSI in db\n", gsup->imsi);
+			gsup_out.cause = GMM_CAUSE_NET_FAIL;
+			break;
+		}
+	} else {
+		gsup_out.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_RESULT;
+		gsup_out.num_auth_vectors = rc;
+	}
+
+	msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP AUC response");
+	osmo_gsup_encode(msg_out, &gsup_out);
+	return osmo_gsup_conn_send(conn, msg_out);
+}
+
+/***********************************************************************
+ * LU Operation State / Structure
+ ***********************************************************************/
+
+static LLIST_HEAD(g_lu_ops);
+
+/*! Receive Cancel Location Result from old VLR/SGSN */
+void lu_op_rx_cancel_old_ack(struct lu_operation *luop,
+			     const struct osmo_gsup_message *gsup)
+{
+	OSMO_ASSERT(luop->state == LU_S_CANCEL_SENT);
+	/* FIXME: Check for spoofing */
+
+	osmo_timer_del(&luop->timer);
+
+	/* FIXME */
+
+	lu_op_tx_insert_subscr_data(luop);
+}
+
+/*! Receive Insert Subscriber Data Result from new VLR/SGSN */
+static void lu_op_rx_insert_subscr_data_ack(struct lu_operation *luop,
+				    const struct osmo_gsup_message *gsup)
+{
+	OSMO_ASSERT(luop->state == LU_S_ISD_SENT);
+	/* FIXME: Check for spoofing */
+
+	osmo_timer_del(&luop->timer);
+
+	/* Subscriber_Present_HLR */
+	/* CS only: Check_SS_required? -> MAP-FW-CHECK_SS_IND.req */
+
+	/* Send final ACK towards inquiring VLR/SGSN */
+	lu_op_tx_ack(luop);
+}
+
+/*! Receive GSUP message for given \ref lu_operation */
+void lu_op_rx_gsup(struct lu_operation *luop,
+		  const struct osmo_gsup_message *gsup)
+{
+	switch (gsup->message_type) {
+	case OSMO_GSUP_MSGT_INSERT_DATA_ERROR:
+		/* FIXME */
+		break;
+	case OSMO_GSUP_MSGT_INSERT_DATA_RESULT:
+		lu_op_rx_insert_subscr_data_ack(luop, gsup);
+		break;
+	case OSMO_GSUP_MSGT_LOCATION_CANCEL_ERROR:
+		/* FIXME */
+		break;
+	case OSMO_GSUP_MSGT_LOCATION_CANCEL_RESULT:
+		lu_op_rx_cancel_old_ack(luop, gsup);
+		break;
+	default:
+		LOGP(DMAIN, LOGL_ERROR, "Unhandled GSUP msg_type 0x%02x\n",
+			gsup->message_type);
+		break;
+	}
+}
+
+/*! Receive Update Location Request, creates new \ref lu_operation */
+static int rx_upd_loc_req(struct osmo_gsup_conn *conn,
+			  const struct osmo_gsup_message *gsup)
+{
+	struct hlr_subscriber *subscr;
+	struct lu_operation *luop = lu_op_alloc_conn(conn);
+	if (!luop) {
+		LOGP(DMAIN, LOGL_ERROR, "LU REQ from conn without addr?\n");
+		return -EINVAL;
+	}
+
+	subscr = &luop->subscr;
+
+	lu_op_statechg(luop, LU_S_LU_RECEIVED);
+
+	switch (gsup->cn_domain) {
+	case OSMO_GSUP_CN_DOMAIN_CS:
+		conn->supports_cs = true;
+		break;
+	default:
+		/* The client didn't send a CN_DOMAIN IE; assume packet-switched in
+		 * accordance with the GSUP spec in osmo-hlr's user manual (section
+		 * 11.6.15 "CN Domain" says "if no CN Domain IE is present within
+		 * a request, the PS Domain is assumed." */
+	case OSMO_GSUP_CN_DOMAIN_PS:
+		conn->supports_ps = true;
+		luop->is_ps = true;
+		break;
+	}
+	llist_add(&luop->list, &g_lu_ops);
+
+	/* Roughly follwing "Process Update_Location_HLR" of TS 09.02 */
+
+	/* check if subscriber is known at all */
+	if (!lu_op_fill_subscr(luop, g_hlr->dbc, gsup->imsi)) {
+		/* Send Error back: Subscriber Unknown in HLR */
+		osmo_strlcpy(luop->subscr.imsi, gsup->imsi, sizeof(luop->subscr.imsi));
+		lu_op_tx_error(luop, GMM_CAUSE_IMSI_UNKNOWN);
+		return 0;
+	}
+
+	/* Check if subscriber is generally permitted on CS or PS
+	 * service (as requested) */
+	if (!luop->is_ps && !luop->subscr.nam_cs) {
+		lu_op_tx_error(luop, GMM_CAUSE_PLMN_NOTALLOWED);
+		return 0;
+	} else if (luop->is_ps && !luop->subscr.nam_ps) {
+		lu_op_tx_error(luop, GMM_CAUSE_GPRS_NOTALLOWED);
+		return 0;
+	}
+
+	/* TODO: Set subscriber tracing = deactive in VLR/SGSN */
+
+#if 0
+	/* Cancel in old VLR/SGSN, if new VLR/SGSN differs from old */
+	if (luop->is_ps == false &&
+	    strcmp(subscr->vlr_number, vlr_number)) {
+		lu_op_tx_cancel_old(luop);
+	} else if (luop->is_ps == true &&
+		   strcmp(subscr->sgsn_number, sgsn_number)) {
+		lu_op_tx_cancel_old(luop);
+	} else
+#endif
+
+	/* Store the VLR / SGSN number with the subscriber, so we know where it was last seen. */
+	LOGP(DAUC, LOGL_DEBUG, "IMSI='%s': storing %s = %s\n",
+	     subscr->imsi, luop->is_ps ? "SGSN number" : "VLR number",
+	     osmo_quote_str((const char*)luop->peer, -1));
+	if (db_subscr_lu(g_hlr->dbc, subscr->id, (const char *)luop->peer, luop->is_ps))
+		LOGP(DAUC, LOGL_ERROR, "IMSI='%s': Cannot update %s in the database\n",
+		     subscr->imsi, luop->is_ps ? "SGSN number" : "VLR number");
+
+	{
+		/* TODO: Subscriber allowed to roam in PLMN? */
+		/* TODO: Update RoutingInfo */
+		/* TODO: Reset Flag MS Purged (cs/ps) */
+		/* TODO: Control_Tracing_HLR / Control_Tracing_HLR_with_SGSN */
+		lu_op_tx_insert_subscr_data(luop);
+	}
+	return 0;
+}
+
+static int rx_purge_ms_req(struct osmo_gsup_conn *conn,
+			   const struct osmo_gsup_message *gsup)
+{
+	struct osmo_gsup_message gsup_reply = {0};
+	struct msgb *msg_out;
+	bool is_ps = false;
+	int rc;
+
+	LOGP(DAUC, LOGL_INFO, "%s: Purge MS (%s)\n", gsup->imsi,
+		is_ps ? "PS" : "CS");
+
+	memcpy(gsup_reply.imsi, gsup->imsi, sizeof(gsup_reply.imsi));
+
+	if (gsup->cn_domain == OSMO_GSUP_CN_DOMAIN_PS)
+		is_ps = true;
+
+	/* FIXME: check if the VLR that sends the purge is the same that
+	 * we have on record. Only update if yes */
+
+	/* Perform the actual update of the DB */
+	rc = db_subscr_purge(g_hlr->dbc, gsup->imsi, true, is_ps);
+
+	if (rc == 0)
+		gsup_reply.message_type = OSMO_GSUP_MSGT_PURGE_MS_RESULT;
+	else if (rc == -ENOENT) {
+		gsup_reply.message_type = OSMO_GSUP_MSGT_PURGE_MS_ERROR;
+		gsup_reply.cause = GMM_CAUSE_IMSI_UNKNOWN;
+	} else {
+		gsup_reply.message_type = OSMO_GSUP_MSGT_PURGE_MS_ERROR;
+		gsup_reply.cause = GMM_CAUSE_NET_FAIL;
+	}
+
+	msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP AUC response");
+	osmo_gsup_encode(msg_out, &gsup_reply);
+	return osmo_gsup_conn_send(conn, msg_out);
+}
+
+static int gsup_send_err_reply(struct osmo_gsup_conn *conn, const char *imsi,
+				enum osmo_gsup_message_type type_in, uint8_t err_cause)
+{
+	int type_err = osmo_gsup_get_err_msg_type(type_in);
+	struct osmo_gsup_message gsup_reply = {0};
+	struct msgb *msg_out;
+
+	if (type_err < 0) {
+		LOGP(DMAIN, LOGL_ERROR, "unable to determine error response for %s\n",
+			osmo_gsup_message_type_name(type_in));
+		return type_err;
+	}
+
+	OSMO_STRLCPY_ARRAY(gsup_reply.imsi, imsi);
+	gsup_reply.message_type = type_err;
+	gsup_reply.cause = err_cause;
+	msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP ERR response");
+	OSMO_ASSERT(msg_out);
+	osmo_gsup_encode(msg_out, &gsup_reply);
+	LOGP(DMAIN, LOGL_NOTICE, "Tx %s\n", osmo_gsup_message_type_name(type_err));
+	return osmo_gsup_conn_send(conn, msg_out);
+}
+
+static int read_cb(struct osmo_gsup_conn *conn, struct msgb *msg)
+{
+	static struct osmo_gsup_message gsup;
+	int rc;
+
+	rc = osmo_gsup_decode(msgb_l2(msg), msgb_l2len(msg), &gsup);
+	if (rc < 0) {
+		LOGP(DMAIN, LOGL_ERROR, "error in GSUP decode: %d\n", rc);
+		return rc;
+	}
+
+	/* 3GPP TS 23.003 Section 2.2 clearly states that an IMSI with less than 5
+	 * digits is impossible.  Even 5 digits is a highly theoretical case */
+	if (strlen(gsup.imsi) < 5)
+		return gsup_send_err_reply(conn, gsup.imsi, gsup.message_type, GMM_CAUSE_INV_MAND_INFO);
+
+	switch (gsup.message_type) {
+	/* requests sent to us */
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_REQUEST:
+		rx_send_auth_info(conn, &gsup, g_hlr->dbc);
+		break;
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST:
+		rx_upd_loc_req(conn, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_PURGE_MS_REQUEST:
+		rx_purge_ms_req(conn, &gsup);
+		break;
+	/* responses to requests sent by us */
+	case OSMO_GSUP_MSGT_DELETE_DATA_ERROR:
+		LOGP(DMAIN, LOGL_ERROR, "Error while deleting subscriber data "
+		     "for IMSI %s\n", gsup.imsi);
+		break;
+	case OSMO_GSUP_MSGT_DELETE_DATA_RESULT:
+		LOGP(DMAIN, LOGL_ERROR, "Deleting subscriber data for IMSI %s\n",
+		     gsup.imsi);
+		break;
+	case OSMO_GSUP_MSGT_PROC_SS_REQUEST:
+	case OSMO_GSUP_MSGT_PROC_SS_RESULT:
+		rx_proc_ss_req(conn, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_PROC_SS_ERROR:
+		rx_proc_ss_error(conn, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_INSERT_DATA_ERROR:
+	case OSMO_GSUP_MSGT_INSERT_DATA_RESULT:
+	case OSMO_GSUP_MSGT_LOCATION_CANCEL_ERROR:
+	case OSMO_GSUP_MSGT_LOCATION_CANCEL_RESULT:
+		{
+			struct lu_operation *luop = lu_op_by_imsi(gsup.imsi,
+								  &g_lu_ops);
+			if (!luop) {
+				LOGP(DMAIN, LOGL_ERROR, "GSUP message %s for "
+				     "unknown IMSI %s\n",
+				     osmo_gsup_message_type_name(gsup.message_type),
+					gsup.imsi);
+				break;
+			}
+			lu_op_rx_gsup(luop, &gsup);
+		}
+		break;
+	default:
+		LOGP(DMAIN, LOGL_DEBUG, "Unhandled GSUP message type %s\n",
+		     osmo_gsup_message_type_name(gsup.message_type));
+		break;
+	}
+	msgb_free(msg);
+	return 0;
+}
+
+static void print_usage()
+{
+	printf("Usage: osmo-hlr\n");
+}
+
+static void print_help()
+{
+	printf("  -h --help                  This text.\n");
+	printf("  -c --config-file filename  The config file to use.\n");
+	printf("  -l --database db-name      The database to use.\n");
+	printf("  -d option --debug=DRLL:DCC:DMM:DRR:DRSL:DNM  Enable debugging.\n");
+	printf("  -D --daemonize             Fork the process into a background daemon.\n");
+	printf("  -s --disable-color         Do not print ANSI colors in the log\n");
+	printf("  -T --timestamp             Prefix every log line with a timestamp.\n");
+	printf("  -e --log-level number      Set a global loglevel.\n");
+	printf("  -V --version               Print the version of OsmoHLR.\n");
+}
+
+static struct {
+	const char *config_file;
+	const char *db_file;
+	bool daemonize;
+} cmdline_opts = {
+	.config_file = "osmo-hlr.cfg",
+	.db_file = "hlr.db",
+	.daemonize = false,
+};
+
+static void handle_options(int argc, char **argv)
+{
+	while (1) {
+		int option_index = 0, c;
+		static struct option long_options[] = {
+			{"help", 0, 0, 'h'},
+			{"config-file", 1, 0, 'c'},
+			{"database", 1, 0, 'l'},
+			{"debug", 1, 0, 'd'},
+			{"daemonize", 0, 0, 'D'},
+			{"disable-color", 0, 0, 's'},
+			{"log-level", 1, 0, 'e'},
+			{"timestamp", 0, 0, 'T'},
+			{"version", 0, 0, 'V' },
+			{0, 0, 0, 0}
+		};
+
+		c = getopt_long(argc, argv, "hc:l:d:Dse:TV",
+				long_options, &option_index);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			print_usage();
+			print_help();
+			exit(0);
+		case 'c':
+			cmdline_opts.config_file = optarg;
+			break;
+		case 'l':
+			cmdline_opts.db_file = optarg;
+			break;
+		case 'd':
+			log_parse_category_mask(osmo_stderr_target, optarg);
+			break;
+		case 'D':
+			cmdline_opts.daemonize = 1;
+			break;
+		case 's':
+			log_set_use_color(osmo_stderr_target, 0);
+			break;
+		case 'e':
+			log_set_log_level(osmo_stderr_target, atoi(optarg));
+			break;
+		case 'T':
+			log_set_print_timestamp(osmo_stderr_target, 1);
+			break;
+		case 'V':
+			print_version(1);
+			exit(0);
+			break;
+		default:
+			/* catch unknown options *as well as* missing arguments. */
+			fprintf(stderr, "Error in command line options. Exiting.\n");
+			exit(-1);
+			break;
+		}
+	}
+}
+
+static void *hlr_ctx = NULL;
+
+static void signal_hdlr(int signal)
+{
+	switch (signal) {
+	case SIGINT:
+		LOGP(DMAIN, LOGL_NOTICE, "Terminating due to SIGINT\n");
+		quit++;
+		break;
+	case SIGUSR1:
+		LOGP(DMAIN, LOGL_DEBUG, "Talloc Report due to SIGUSR1\n");
+		talloc_report_full(hlr_ctx, stderr);
+		break;
+	}
+}
+
+static const char vlr_copyright[] =
+	"Copyright (C) 2016, 2017 by Harald Welte, sysmocom s.f.m.c. GmbH\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 vty_app_info vty_info = {
+	.name 		= "OsmoHLR",
+	.version	= PACKAGE_VERSION,
+	.copyright	= vlr_copyright,
+	.is_config_node	= hlr_vty_is_config_node,
+	.go_parent_cb   = hlr_vty_go_parent,
+};
+
+int main(int argc, char **argv)
+{
+	int rc;
+
+	/* Track the use of talloc NULL memory contexts */
+	talloc_enable_null_tracking();
+
+	hlr_ctx = talloc_named_const(NULL, 1, "OsmoHLR");
+	msgb_talloc_ctx_init(hlr_ctx, 0);
+	vty_info.tall_ctx = hlr_ctx;
+
+	g_hlr = talloc_zero(hlr_ctx, struct hlr);
+	INIT_LLIST_HEAD(&g_hlr->euse_list);
+	INIT_LLIST_HEAD(&g_hlr->iuse_list);
+	INIT_LLIST_HEAD(&g_hlr->ss_sessions);
+	INIT_LLIST_HEAD(&g_hlr->ussd_routes);
+
+	rc = osmo_init_logging2(hlr_ctx, &hlr_log_info);
+	if (rc < 0) {
+		fprintf(stderr, "Error initializing logging\n");
+		exit(1);
+	}
+
+	vty_init(&vty_info);
+	ctrl_vty_init(hlr_ctx);
+	handle_options(argc, argv);
+	hlr_vty_init(&hlr_log_info);
+
+	rc = vty_read_config_file(cmdline_opts.config_file, NULL);
+	if (rc < 0) {
+		LOGP(DMAIN, LOGL_FATAL,
+		     "Failed to parse the config file: '%s'\n",
+		     cmdline_opts.config_file);
+		return rc;
+	}
+
+	/* start telnet after reading config for vty_get_bind_addr() */
+	rc = telnet_init_dynif(hlr_ctx, NULL, vty_get_bind_addr(),
+			       OSMO_VTY_PORT_HLR);
+	if (rc < 0)
+		return rc;
+
+	LOGP(DMAIN, LOGL_NOTICE, "hlr starting\n");
+
+	rc = rand_init();
+	if (rc < 0) {
+		LOGP(DMAIN, LOGL_FATAL, "Error initializing random source\n");
+		exit(1);
+	}
+
+	g_hlr->dbc = db_open(hlr_ctx, cmdline_opts.db_file, true);
+	if (!g_hlr->dbc) {
+		LOGP(DMAIN, LOGL_FATAL, "Error opening database\n");
+		exit(1);
+	}
+
+	g_hlr->gs = osmo_gsup_server_create(hlr_ctx, g_hlr->gsup_bind_addr, OSMO_GSUP_PORT,
+					    read_cb, &g_lu_ops, g_hlr);
+	if (!g_hlr->gs) {
+		LOGP(DMAIN, LOGL_FATAL, "Error starting GSUP server\n");
+		exit(1);
+	}
+
+	g_hlr->ctrl_bind_addr = ctrl_vty_get_bind_addr();
+	g_hlr->ctrl = hlr_controlif_setup(g_hlr);
+
+	osmo_init_ignore_signals();
+	signal(SIGINT, &signal_hdlr);
+	signal(SIGUSR1, &signal_hdlr);
+
+	if (cmdline_opts.daemonize) {
+		rc = osmo_daemonize();
+		if (rc < 0) {
+			perror("Error during daemonize");
+			exit(1);
+		}
+	}
+
+	while (!quit)
+		osmo_select_main(0);
+
+	osmo_gsup_server_destroy(g_hlr->gs);
+	db_close(g_hlr->dbc);
+	log_fini();
+
+	/**
+	 * Report the heap state of root context, then free,
+	 * so both ASAN and Valgrind are happy...
+	 */
+	talloc_report_full(hlr_ctx, stderr);
+	talloc_free(hlr_ctx);
+
+	/* FIXME: VTY code still uses NULL-context */
+	talloc_free(tall_vty_ctx);
+
+	/**
+	 * Report the heap state of NULL context, then free,
+	 * so both ASAN and Valgrind are happy...
+	 */
+	talloc_report_full(NULL, stderr);
+	talloc_disable_null_tracking();
+
+	return 0;
+}
diff --git a/src/hlr.h b/src/hlr.h
new file mode 100644
index 0000000..315c3dd
--- /dev/null
+++ b/src/hlr.h
@@ -0,0 +1,57 @@
+/* OsmoHLR generic header */
+
+/* (C) 2017 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Max Suraev <msuraev@sysmocom.de>
+ *
+ * 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/>.
+ *
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <osmocom/core/linuxlist.h>
+
+struct hlr_euse;
+
+struct hlr {
+	/* GSUP server pointer */
+	struct osmo_gsup_server *gs;
+
+	/* DB context */
+	struct db_context *dbc;
+
+	/* Control Interface */
+	struct ctrl_handle *ctrl;
+	const char *ctrl_bind_addr;
+
+	/* Local bind addr */
+	char *gsup_bind_addr;
+
+	struct llist_head euse_list;
+	struct hlr_euse *euse_default;
+	struct llist_head iuse_list;
+
+	struct llist_head ussd_routes;
+
+	struct llist_head ss_sessions;
+};
+
+extern struct hlr *g_hlr;
+
+struct hlr_subscriber;
+
+void osmo_hlr_subscriber_update_notify(struct hlr_subscriber *subscr);
diff --git a/src/hlr_db_tool.c b/src/hlr_db_tool.c
new file mode 100644
index 0000000..e83b098
--- /dev/null
+++ b/src/hlr_db_tool.c
@@ -0,0 +1,439 @@
+/* (C) 2017 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 <signal.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+
+#include "logging.h"
+#include "db.h"
+#include "rand.h"
+
+struct hlr_db_tool_ctx {
+	/* DB context */
+	struct db_context *dbc;
+};
+
+struct hlr_db_tool_ctx *g_hlr_db_tool_ctx;
+
+static struct {
+	const char *db_file;
+	bool bootstrap;
+	const char *import_nitb_db;
+} cmdline_opts = {
+	.db_file = "hlr.db",
+};
+
+static void print_help()
+{
+	printf("\n");
+	printf("Usage: osmo-hlr-db-tool [-l <hlr.db>] [create|import-nitb-db <nitb.db>]\n");
+	printf("  -l --database db-name      The OsmoHLR database to use, default '%s'.\n",
+	       cmdline_opts.db_file);
+	printf("  -h --help                  This text.\n");
+	printf("  -d option --debug=DMAIN:DDB:DAUC  Enable debugging.\n");
+	printf("  -s --disable-color         Do not print ANSI colors in the log\n");
+	printf("  -T --timestamp             Prefix every log line with a timestamp.\n");
+	printf("  -e --log-level number      Set a global loglevel.\n");
+	printf("  -V --version               Print the version of OsmoHLR-db-tool.\n");
+	printf("\n");
+	printf("Commands:\n");
+	printf("\n");
+	printf("  create                     Create an empty OsmoHLR database.\n");
+	printf("                             (All commands imply this if none exists yet.)\n");
+	printf("\n");
+	printf("  import-nitb-db <nitb.db>   Add OsmoNITB db's subscribers to OsmoHLR db.\n");
+	printf("                             Be aware that the import is lossy, only the\n");
+	printf("                             IMSI, MSISDN, nam_cs/ps and 2G auth data are set.\n");
+}
+
+static void print_version(int print_copyright)
+{
+	printf("OsmoHLR-db-tool version %s\n", PACKAGE_VERSION);
+	if (print_copyright)
+		printf("\n"
+       "Copyright (C) 2017 by sysmocom - s.f.m.c. GmbH\n"
+       "License AGPLv3+: GNU AGPL version 3 or later <http://gnu.org/licenses/agpl-3.0.html>\n"
+       "This is free software: you are free to change and redistribute it.\n"
+       "There is NO WARRANTY, to the extent permitted by law.\n"
+       "\n");
+}
+
+static void handle_options(int argc, char **argv)
+{
+	const char *cmd;
+
+	while (1) {
+		int option_index = 0, c;
+		static struct option long_options[] = {
+			{"help", 0, 0, 'h'},
+			{"database", 1, 0, 'l'},
+			{"debug", 1, 0, 'd'},
+			{"disable-color", 0, 0, 's'},
+			{"timestamp", 0, 0, 'T'},
+			{"log-level", 1, 0, 'e'},
+			{"version", 0, 0, 'V' },
+			{0, 0, 0, 0}
+		};
+
+		c = getopt_long(argc, argv, "hl:d:sTe:V",
+				long_options, &option_index);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'l':
+			cmdline_opts.db_file = optarg;
+			break;
+		case 'd':
+			log_parse_category_mask(osmo_stderr_target, optarg);
+			break;
+		case 's':
+			log_set_use_color(osmo_stderr_target, 0);
+			break;
+		case 'T':
+			log_set_print_timestamp(osmo_stderr_target, 1);
+			break;
+		case 'e':
+			log_set_log_level(osmo_stderr_target, atoi(optarg));
+			break;
+		case 'V':
+			print_version(1);
+			exit(EXIT_SUCCESS);
+			break;
+		default:
+			/* catch unknown options *as well as* missing arguments. */
+			fprintf(stderr, "Error in command line options. Exiting.\n");
+			exit(EXIT_FAILURE);
+			break;
+		}
+	}
+
+	if (argc - optind <= 0) {
+		fprintf(stderr, "Error: You must specify a command.\n");
+		print_help();
+		exit(EXIT_FAILURE);
+	}
+
+	cmd = argv[optind++];
+
+	if (!strcmp(cmd, "create")) {
+		/* Nothing to do, just run the main program to open the database without running any
+		 * action, which will bootstrap all tables. */
+	} else if (!strcmp(cmd, "import-nitb-db")) {
+		if (argc - optind < 1) {
+			fprintf(stderr, "You must specify an input db file\n");
+			print_help();
+			exit(EXIT_FAILURE);
+		}
+		cmdline_opts.import_nitb_db = argv[optind++];
+	} else {
+		fprintf(stderr, "Error: Unknown command `%s'\n", cmd);
+		print_help();
+		exit(EXIT_FAILURE);
+	}
+
+	if (argc - optind > 0) {
+		fprintf(stderr, "Too many arguments: '%s'\n", argv[optind]);
+		print_help();
+		exit(EXIT_FAILURE);
+	}
+}
+
+static void signal_hdlr(int signal)
+{
+	switch (signal) {
+	case SIGINT:
+		LOGP(DMAIN, LOGL_NOTICE, "Terminating due to SIGINT\n");
+		db_close(g_hlr_db_tool_ctx->dbc);
+		log_fini();
+		talloc_report_full(g_hlr_db_tool_ctx, stderr);
+		exit(EXIT_SUCCESS);
+		break;
+	case SIGUSR1:
+		LOGP(DMAIN, LOGL_DEBUG, "Talloc Report due to SIGUSR1\n");
+		talloc_report_full(g_hlr_db_tool_ctx, stderr);
+		break;
+	}
+}
+
+sqlite3 *open_nitb_db(const char *filename)
+{
+	int rc;
+	sqlite3 *nitb_db = NULL;
+
+	rc = sqlite3_open(filename, &nitb_db);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Unable to open OsmoNITB DB %s; rc = %d\n", filename, rc);
+		return NULL;
+	}
+
+	return nitb_db;
+}
+
+enum nitb_stmt {
+	NITB_SELECT_SUBSCR,
+	NITB_SELECT_AUTH_KEYS,
+};
+
+static const char *nitb_stmt_sql[] = {
+	[NITB_SELECT_SUBSCR] =
+		"SELECT imsi, id, extension, authorized"
+		" FROM Subscriber"
+		" ORDER BY id",
+	[NITB_SELECT_AUTH_KEYS] =
+		"SELECT algorithm_id, a3a8_ki from authkeys"
+		" WHERE subscriber_id = $subscr_id",
+};
+
+sqlite3_stmt *nitb_stmt[ARRAY_SIZE(nitb_stmt_sql)] = {};
+
+size_t _dbd_decode_binary(const unsigned char *in, unsigned char *out);
+
+void import_nitb_subscr_aud(sqlite3 *nitb_db, const char *imsi, int64_t nitb_id, int64_t hlr_id)
+{
+	int rc;
+	struct db_context *dbc = g_hlr_db_tool_ctx->dbc;
+	sqlite3_stmt *stmt;
+
+	int count = 0;
+
+	stmt = nitb_stmt[NITB_SELECT_AUTH_KEYS];
+	if (!db_bind_int(stmt, NULL, nitb_id))
+		return;
+
+	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
+		const void *blob;
+		unsigned int blob_size;
+		static unsigned char buf[4096];
+		static char ki[128];
+		int decoded_size;
+		struct sub_auth_data_str aud2g = {
+			.type = OSMO_AUTH_TYPE_GSM,
+			.algo = OSMO_AUTH_ALG_NONE,
+			.u.gsm.ki = ki,
+		};
+
+		aud2g.algo = sqlite3_column_int(stmt, 0);
+
+		if (count) {
+			LOGP(DDB, LOGL_ERROR,
+			     "Warning: subscriber has more than one auth key,"
+			     " importing only the first key, for IMSI=%s\n",
+			     imsi);
+			break;
+		}
+
+		blob = sqlite3_column_blob(stmt, 1);
+		blob_size = sqlite3_column_bytes(stmt, 1);
+
+		if (blob_size > sizeof(buf)) {
+			LOGP(DDB, LOGL_ERROR,
+			     "OsmoNITB import to %s: Cannot import auth data for IMSI %s:"
+			     " too large blob: %u\n",
+			     dbc->fname, imsi, blob_size);
+			db_remove_reset(stmt);
+			continue;
+		}
+
+		decoded_size = _dbd_decode_binary(blob, buf);
+		osmo_strlcpy(ki, osmo_hexdump_nospc(buf, decoded_size), sizeof(ki));
+
+		db_subscr_update_aud_by_id(dbc, hlr_id, &aud2g);
+		count ++;
+	}
+
+	if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
+		LOGP(DDB, LOGL_ERROR, "OsmoNITB DB: SQL error: (%d) %s,"
+		     " during stmt '%s'",
+		     rc, sqlite3_errmsg(nitb_db),
+		     nitb_stmt_sql[NITB_SELECT_AUTH_KEYS]);
+	}
+
+	db_remove_reset(stmt);
+}
+
+void import_nitb_subscr(sqlite3 *nitb_db, sqlite3_stmt *stmt)
+{
+	struct db_context *dbc = g_hlr_db_tool_ctx->dbc;
+	int rc;
+	struct hlr_subscriber subscr;
+
+	int64_t nitb_id;
+	int64_t imsi;
+	char imsi_str[32];
+	bool authorized;
+
+	imsi = sqlite3_column_int64(stmt, 0);
+
+	snprintf(imsi_str, sizeof(imsi_str), "%"PRId64, imsi);
+
+	rc = db_subscr_create(dbc, imsi_str);
+	if (rc < 0) {
+		LOGP(DDB, LOGL_ERROR, "OsmoNITB DB import to %s: failed to create IMSI %s: %d: %s\n",
+		     dbc->fname,
+		     imsi_str,
+		     rc,
+		     strerror(-rc));
+		/* on error, still attempt to continue */
+	}
+
+	nitb_id = sqlite3_column_int64(stmt, 1);
+	copy_sqlite3_text_to_buf(subscr.msisdn, stmt, 2);
+	authorized = sqlite3_column_int(stmt, 3) ? true : false;
+
+	db_subscr_update_msisdn_by_imsi(dbc, imsi_str, subscr.msisdn);
+	db_subscr_nam(dbc, imsi_str, authorized, true);
+	db_subscr_nam(dbc, imsi_str, authorized, false);
+
+	/* find the just created id */
+	rc = db_subscr_get_by_imsi(dbc, imsi_str, &subscr);
+	if (rc < 0) {
+		LOGP(DDB, LOGL_ERROR, "OsmoNITB DB import to %s: created IMSI %s,"
+		     " but failed to get new subscriber id: %d: %s\n",
+		     dbc->fname,
+		     imsi_str,
+		     rc,
+		     strerror(-rc));
+		return;
+	}
+
+	OSMO_ASSERT(!strcmp(imsi_str, subscr.imsi));
+
+	import_nitb_subscr_aud(nitb_db, imsi_str, nitb_id, subscr.id);
+}
+
+int import_nitb_db(void)
+{
+	int i;
+	int ret;
+	int rc;
+	const char *sql;
+	sqlite3_stmt *stmt;
+
+	sqlite3 *nitb_db = open_nitb_db(cmdline_opts.import_nitb_db);
+
+	if (!nitb_db)
+		return -1;
+	ret = 0;
+
+	for (i = 0; i < ARRAY_SIZE(nitb_stmt_sql); i++) {
+		sql = nitb_stmt_sql[i];
+		rc = sqlite3_prepare_v2(nitb_db, sql, -1, &nitb_stmt[i], NULL);
+		if (rc != SQLITE_OK) {
+			LOGP(DDB, LOGL_ERROR, "OsmoNITB DB: Unable to prepare SQL statement '%s'\n", sql);
+			ret = -1;
+			goto out_free;
+		}
+	}
+
+	stmt = nitb_stmt[NITB_SELECT_SUBSCR];
+
+	while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
+		import_nitb_subscr(nitb_db, stmt);
+		/* On failure, carry on with the rest. */
+	}
+	if (rc != SQLITE_DONE) {
+		LOGP(DDB, LOGL_ERROR, "OsmoNITB DB: SQL error: (%d) %s,"
+		     " during stmt '%s'",
+		     rc, sqlite3_errmsg(nitb_db),
+		     nitb_stmt_sql[NITB_SELECT_SUBSCR]);
+		goto out_free;
+	}
+
+	db_remove_reset(stmt);
+	sqlite3_finalize(stmt);
+
+out_free:
+	sqlite3_close(nitb_db);
+	return ret;
+}
+
+int main(int argc, char **argv)
+{
+	int rc;
+	int (*main_action)(void);
+	main_action = NULL;
+
+	g_hlr_db_tool_ctx = talloc_zero(NULL, struct hlr_db_tool_ctx);
+	OSMO_ASSERT(g_hlr_db_tool_ctx);
+	talloc_set_name_const(g_hlr_db_tool_ctx, "OsmoHLR-db-tool");
+
+	rc = osmo_init_logging2(g_hlr_db_tool_ctx, &hlr_log_info);
+	if (rc < 0) {
+		fprintf(stderr, "Error initializing logging\n");
+		exit(EXIT_FAILURE);
+	}
+
+	handle_options(argc, argv);
+
+	if (cmdline_opts.import_nitb_db) {
+		if (main_action)
+			goto too_many_actions;
+		main_action = import_nitb_db;
+	}
+	/* Future: add more main_actions, besides import-nitb-db, here.
+	 * For command 'create', no action is required. */
+
+	/* Just in case any db actions need randomness */
+	rc = rand_init();
+	if (rc < 0) {
+		LOGP(DMAIN, LOGL_FATAL, "Error initializing random source\n");
+		exit(EXIT_FAILURE);
+	}
+
+	g_hlr_db_tool_ctx->dbc = db_open(g_hlr_db_tool_ctx, cmdline_opts.db_file, true);
+	if (!g_hlr_db_tool_ctx->dbc) {
+		LOGP(DMAIN, LOGL_FATAL, "Error opening database\n");
+		exit(EXIT_FAILURE);
+	}
+
+	osmo_init_ignore_signals();
+	signal(SIGINT, &signal_hdlr);
+	signal(SIGUSR1, &signal_hdlr);
+
+	rc = 0;
+	if (main_action)
+		rc = (*main_action)();
+
+	db_close(g_hlr_db_tool_ctx->dbc);
+	log_fini();
+	exit(rc ? EXIT_FAILURE : EXIT_SUCCESS);
+
+too_many_actions:
+	fprintf(stderr, "Too many actions requested.\n");
+	log_fini();
+	exit(EXIT_FAILURE);
+}
+
+/* stubs */
+void lu_op_alloc_conn(void) { OSMO_ASSERT(0); }
+void lu_op_tx_del_subscr_data(void) { OSMO_ASSERT(0); }
+void lu_op_free(void) { OSMO_ASSERT(0); }
diff --git a/src/hlr_ussd.c b/src/hlr_ussd.c
new file mode 100644
index 0000000..ba373f3
--- /dev/null
+++ b/src/hlr_ussd.c
@@ -0,0 +1,579 @@
+/* OsmoHLR SS/USSD implementation */
+
+/* (C) 2018 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 <osmocom/core/talloc.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsm/gsm0480.h>
+#include <osmocom/gsm/protocol/gsm_04_80.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+
+#include "hlr.h"
+#include "hlr_ussd.h"
+#include "gsup_server.h"
+#include "gsup_router.h"
+#include "logging.h"
+
+/***********************************************************************
+ * core data structures expressing config from VTY
+ ***********************************************************************/
+
+struct hlr_euse *euse_find(struct hlr *hlr, const char *name)
+{
+	struct hlr_euse *euse;
+
+	llist_for_each_entry(euse, &hlr->euse_list, list) {
+		if (!strcmp(euse->name, name))
+			return euse;
+	}
+	return NULL;
+}
+
+struct hlr_euse *euse_alloc(struct hlr *hlr, const char *name)
+{
+	struct hlr_euse *euse = euse_find(hlr, name);
+	if (euse)
+		return NULL;
+
+	euse = talloc_zero(hlr, struct hlr_euse);
+	euse->name = talloc_strdup(euse, name);
+	euse->hlr = hlr;
+	llist_add_tail(&euse->list, &hlr->euse_list);
+
+	return euse;
+}
+
+void euse_del(struct hlr_euse *euse)
+{
+	llist_del(&euse->list);
+	talloc_free(euse);
+}
+
+
+struct hlr_ussd_route *ussd_route_find_prefix(struct hlr *hlr, const char *prefix)
+{
+	struct hlr_ussd_route *rt;
+
+	llist_for_each_entry(rt, &hlr->ussd_routes, list) {
+		if (!strcmp(rt->prefix, prefix))
+			return rt;
+	}
+	return NULL;
+}
+
+struct hlr_ussd_route *ussd_route_prefix_alloc_int(struct hlr *hlr, const char *prefix,
+						   const struct hlr_iuse *iuse)
+{
+	struct hlr_ussd_route *rt;
+
+	if (ussd_route_find_prefix(hlr, prefix))
+		return NULL;
+
+	rt = talloc_zero(hlr, struct hlr_ussd_route);
+	rt->prefix = talloc_strdup(rt, prefix);
+	rt->u.iuse = iuse;
+	llist_add_tail(&rt->list, &hlr->ussd_routes);
+
+	return rt;
+}
+
+struct hlr_ussd_route *ussd_route_prefix_alloc_ext(struct hlr *hlr, const char *prefix,
+						   struct hlr_euse *euse)
+{
+	struct hlr_ussd_route *rt;
+
+	if (ussd_route_find_prefix(hlr, prefix))
+		return NULL;
+
+	rt = talloc_zero(hlr, struct hlr_ussd_route);
+	rt->prefix = talloc_strdup(rt, prefix);
+	rt->is_external = true;
+	rt->u.euse = euse;
+	llist_add_tail(&rt->list, &hlr->ussd_routes);
+
+	return rt;
+}
+
+void ussd_route_del(struct hlr_ussd_route *rt)
+{
+	llist_del(&rt->list);
+	talloc_free(rt);
+}
+
+static struct hlr_ussd_route *ussd_route_lookup_7bit(struct hlr *hlr, const char *ussd_code)
+{
+	struct hlr_ussd_route *rt;
+	llist_for_each_entry(rt, &hlr->ussd_routes, list) {
+		if (!strncmp(ussd_code, rt->prefix, strlen(rt->prefix))) {
+			LOGP(DSS, LOGL_DEBUG, "Found %s '%s' (prefix '%s') for USSD "
+				"Code '%s'\n", rt->is_external ? "EUSE" : "IUSE",
+				rt->is_external ? rt->u.euse->name : rt->u.iuse->name,
+				rt->prefix, ussd_code);
+			return rt;
+		}
+	}
+
+	LOGP(DSS, LOGL_DEBUG, "Could not find Route for USSD Code '%s'\n", ussd_code);
+	return NULL;
+}
+
+/***********************************************************************
+ * handling functions for individual GSUP messages
+ ***********************************************************************/
+
+#define LOGPSS(ss, lvl, fmt, args...) \
+	LOGP(DSS, lvl, "%s/0x%08x: " fmt, (ss)->imsi, (ss)->session_id, ## args)
+
+struct ss_session {
+	/* link us to hlr->ss_sessions */
+	struct llist_head list;
+	/* imsi of this session */
+	char imsi[GSM23003_IMSI_MAX_DIGITS+2];
+	/* ID of this session (unique per IMSI) */
+	uint32_t session_id;
+	/* state of the session */
+	enum osmo_gsup_session_state state;
+	/* time-out when we will delete the session */
+	struct osmo_timer_list timeout;
+
+	/* is this USSD for an external handler (EUSE): true */
+	bool is_external;
+	union {
+		/* external USSD Entity responsible for this session */
+		struct hlr_euse *euse;
+		/* internal USSD Entity responsible for this session */
+		const struct hlr_iuse *iuse;
+	} u;
+
+	/* we don't keep a pointer to the osmo_gsup_{route,conn} towards the MSC/VLR here,
+	 * as this might change during inter-VLR hand-over, and we simply look-up the serving MSC/VLR
+	 * every time we receive an USSD component from the EUSE */
+};
+
+struct ss_session *ss_session_find(struct hlr *hlr, const char *imsi, uint32_t session_id)
+{
+	struct ss_session *ss;
+	llist_for_each_entry(ss, &hlr->ss_sessions, list) {
+		if (!strcmp(ss->imsi, imsi) && ss->session_id == session_id)
+			return ss;
+	}
+	return NULL;
+}
+
+void ss_session_free(struct ss_session *ss)
+{
+	osmo_timer_del(&ss->timeout);
+	llist_del(&ss->list);
+	talloc_free(ss);
+}
+
+static void ss_session_timeout(void *data)
+{
+	struct ss_session *ss = data;
+
+	LOGPSS(ss, LOGL_NOTICE, "SS Session Timeout, destroying\n");
+	/* FIXME: should we send a ReturnError component to the MS? */
+	ss_session_free(ss);
+}
+
+struct ss_session *ss_session_alloc(struct hlr *hlr, const char *imsi, uint32_t session_id)
+{
+	struct ss_session *ss;
+
+	OSMO_ASSERT(!ss_session_find(hlr, imsi, session_id));
+
+	ss = talloc_zero(hlr, struct ss_session);
+	OSMO_ASSERT(ss);
+
+	OSMO_STRLCPY_ARRAY(ss->imsi, imsi);
+	ss->session_id = session_id;
+	osmo_timer_setup(&ss->timeout, ss_session_timeout, ss);
+	/* NOTE: The timeout is currently global and not refreshed with subsequent messages
+	 * within the SS/USSD session.  So 30s after the initial SS message, the session will
+	 * timeout! */
+	osmo_timer_schedule(&ss->timeout, 30, 0);
+
+	llist_add_tail(&ss->list, &hlr->ss_sessions);
+	return ss;
+}
+
+/***********************************************************************
+ * handling functions for encoding SS messages + wrapping them in GSUP
+ ***********************************************************************/
+
+static int ss_tx_to_ms(struct ss_session *ss, enum osmo_gsup_message_type gsup_msg_type,
+			bool final, struct msgb *ss_msg)
+
+{
+	struct osmo_gsup_message resp = {0};
+	struct msgb *resp_msg;
+
+	resp.message_type = gsup_msg_type;
+	OSMO_STRLCPY_ARRAY(resp.imsi, ss->imsi);
+	if (final)
+		resp.session_state = OSMO_GSUP_SESSION_STATE_END;
+	else
+		resp.session_state = OSMO_GSUP_SESSION_STATE_CONTINUE;
+	resp.session_id = ss->session_id;
+	if (ss_msg) {
+		resp.ss_info = msgb_data(ss_msg);
+		resp.ss_info_len = msgb_length(ss_msg);
+	}
+
+	resp_msg = gsm0480_msgb_alloc_name(__func__);
+	OSMO_ASSERT(resp_msg);
+	osmo_gsup_encode(resp_msg, &resp);
+	msgb_free(ss_msg);
+
+	/* FIXME: resolve this based on the database vlr_addr */
+	return osmo_gsup_addr_send(g_hlr->gs, (uint8_t *)"MSC-00-00-00-00-00-00", 22, resp_msg);
+}
+
+#if 0
+static int ss_tx_reject(struct ss_session *ss, int invoke_id, uint8_t problem_tag,
+			uint8_t problem_code)
+{
+	struct msgb *msg = gsm0480_gen_reject(invoke_id, problem_tag, problem_code);
+	LOGPSS(ss, LOGL_NOTICE, "Tx Reject(%u, 0x%02x, 0x%02x)\n", invoke_id,
+		problem_tag, problem_code);
+	OSMO_ASSERT(msg);
+	return ss_tx_to_ms(ss, OSMO_GSUP_MSGT_PROC_SS_RESULT, true, msg);
+}
+#endif
+
+static int ss_tx_error(struct ss_session *ss, uint8_t invoke_id, uint8_t error_code)
+{
+	struct msgb *msg = gsm0480_gen_return_error(invoke_id, error_code);
+	LOGPSS(ss, LOGL_NOTICE, "Tx ReturnError(%u, 0x%02x)\n", invoke_id, error_code);
+	OSMO_ASSERT(msg);
+	return ss_tx_to_ms(ss, OSMO_GSUP_MSGT_PROC_SS_RESULT, true, msg);
+}
+
+static int ss_tx_ussd_7bit(struct ss_session *ss, bool final, uint8_t invoke_id, const char *text)
+{
+	struct msgb *msg = gsm0480_gen_ussd_resp_7bit(invoke_id, text);
+	LOGPSS(ss, LOGL_INFO, "Tx USSD '%s'\n", text);
+	OSMO_ASSERT(msg);
+	return ss_tx_to_ms(ss, OSMO_GSUP_MSGT_PROC_SS_RESULT, final, msg);
+}
+
+/***********************************************************************
+ * Internal USSD Handlers
+ ***********************************************************************/
+
+#include "db.h"
+
+static int handle_ussd_own_msisdn(struct osmo_gsup_conn *conn, struct ss_session *ss,
+				  const struct osmo_gsup_message *gsup, const struct ss_request *req)
+{
+	struct hlr_subscriber subscr;
+	char buf[GSM0480_USSD_7BIT_STRING_LEN+1];
+	int rc;
+
+	rc = db_subscr_get_by_imsi(g_hlr->dbc, ss->imsi, &subscr);
+	switch (rc) {
+	case 0:
+		if (strlen(subscr.msisdn) == 0)
+			snprintf(buf, sizeof(buf), "You have no MSISDN!");
+		else
+			snprintf(buf, sizeof(buf), "Your extension is %s", subscr.msisdn);
+		ss_tx_ussd_7bit(ss, true, req->invoke_id, buf);
+		break;
+	case -ENOENT:
+		ss_tx_error(ss, true, GSM0480_ERR_CODE_UNKNOWN_SUBSCRIBER);
+		break;
+	case -EIO:
+	default:
+		ss_tx_error(ss, true, GSM0480_ERR_CODE_SYSTEM_FAILURE);
+		break;
+	}
+	return 0;
+}
+
+static int handle_ussd_own_imsi(struct osmo_gsup_conn *conn, struct ss_session *ss,
+				const struct osmo_gsup_message *gsup, const struct ss_request *req)
+{
+	char buf[GSM0480_USSD_7BIT_STRING_LEN+1];
+	snprintf(buf, sizeof(buf), "Your IMSI is %s", ss->imsi);
+	ss_tx_ussd_7bit(ss, true, req->invoke_id, buf);
+	return 0;
+}
+
+
+static const struct hlr_iuse hlr_iuses[] = {
+	{
+		.name = "own-msisdn",
+		.handle_ussd = handle_ussd_own_msisdn,
+	},
+	{
+		.name = "own-imsi",
+		.handle_ussd = handle_ussd_own_imsi,
+	},
+};
+
+const struct hlr_iuse *iuse_find(const char *name)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(hlr_iuses); i++) {
+		const struct hlr_iuse *iuse = &hlr_iuses[i];
+		if (!strcmp(name, iuse->name))
+			return iuse;
+	}
+	return NULL;
+}
+
+
+/***********************************************************************
+ * handling functions for individual GSUP messages
+ ***********************************************************************/
+
+static bool ss_op_is_ussd(uint8_t opcode)
+{
+	switch (opcode) {
+	case GSM0480_OP_CODE_PROCESS_USS_DATA:
+	case GSM0480_OP_CODE_PROCESS_USS_REQ:
+	case GSM0480_OP_CODE_USS_REQUEST:
+	case GSM0480_OP_CODE_USS_NOTIFY:
+		return true;
+	default:
+		return false;
+	}
+}
+
+/* is this GSUP connection an EUSE (true) or not (false)? */
+static bool conn_is_euse(struct osmo_gsup_conn *conn)
+{
+	int rc;
+	uint8_t *addr;
+
+	rc = osmo_gsup_conn_ccm_get(conn, &addr, IPAC_IDTAG_SERNR);
+	if (rc <= 5)
+		return false;
+	if (!strncmp((char *)addr, "EUSE-", 5))
+		return true;
+	else
+		return false;
+}
+
+static struct hlr_euse *euse_by_conn(struct osmo_gsup_conn *conn)
+{
+	int rc;
+	char *addr;
+	struct hlr *hlr = conn->server->priv;
+
+	rc = osmo_gsup_conn_ccm_get(conn, (uint8_t **) &addr, IPAC_IDTAG_SERNR);
+	if (rc <= 5)
+		return NULL;
+	if (strncmp(addr, "EUSE-", 5))
+		return NULL;
+
+	return euse_find(hlr, addr+5);
+}
+
+static int handle_ss(struct ss_session *ss, const struct osmo_gsup_message *gsup,
+			const struct ss_request *req)
+{
+	uint8_t comp_type = gsup->ss_info[0];
+
+	LOGPSS(ss, LOGL_INFO, "SS CompType=%s, OpCode=%s\n",
+		gsm0480_comp_type_name(comp_type), gsm0480_op_code_name(req->opcode));
+
+	/**
+	 * FIXME: As we don't store any SS related information
+	 * (e.g. call forwarding preferences) in the database,
+	 * we don't handle "structured" SS requests at all.
+	 */
+	LOGPSS(ss, LOGL_NOTICE, "Structured SS requests are not supported, rejecting...\n");
+	ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_FACILITY_NOT_SUPPORTED);
+	return -ENOTSUP;
+}
+
+/* Handle a USSD GSUP message for a given SS Session received from VLR or EUSE */
+static int handle_ussd(struct osmo_gsup_conn *conn, struct ss_session *ss,
+			const struct osmo_gsup_message *gsup, const struct ss_request *req)
+{
+	uint8_t comp_type = gsup->ss_info[0];
+	struct msgb *msg_out;
+	bool is_euse_originated = conn_is_euse(conn);
+
+	LOGPSS(ss, LOGL_INFO, "USSD CompType=%s, OpCode=%s '%s'\n",
+		gsm0480_comp_type_name(comp_type), gsm0480_op_code_name(req->opcode),
+		req->ussd_text);
+
+	if ((ss->is_external && !ss->u.euse) || !ss->u.iuse) {
+		LOGPSS(ss, LOGL_NOTICE, "USSD for unknown code '%s'\n", req->ussd_text);
+		ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_SS_NOT_AVAILABLE);
+		return 0;
+	}
+
+	if (is_euse_originated) {
+		msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP USSD FW");
+		OSMO_ASSERT(msg_out);
+		/* Received from EUSE, Forward to VLR */
+		osmo_gsup_encode(msg_out, gsup);
+		/* FIXME: resolve this based on the database vlr_addr */
+		osmo_gsup_addr_send(conn->server, (uint8_t *)"MSC-00-00-00-00-00-00", 22, msg_out);
+	} else {
+		/* Received from VLR (MS) */
+		if (ss->is_external) {
+			/* Forward to EUSE */
+			char addr[128];
+			strcpy(addr, "EUSE-");
+			osmo_strlcpy(addr+5, ss->u.euse->name, sizeof(addr)-5);
+			conn = gsup_route_find(conn->server, (uint8_t *)addr, strlen(addr)+1);
+			if (!conn) {
+				LOGPSS(ss, LOGL_ERROR, "Cannot find conn for EUSE %s\n", addr);
+				ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_SYSTEM_FAILURE);
+			} else {
+				msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP USSD FW");
+				OSMO_ASSERT(msg_out);
+				osmo_gsup_encode(msg_out, gsup);
+				osmo_gsup_conn_send(conn, msg_out);
+			}
+		} else {
+			/* Handle internally */
+			ss->u.iuse->handle_ussd(conn, ss, gsup, req);
+		}
+	}
+
+	return 0;
+}
+
+
+/* this function is called for any SS_REQ/SS_RESP messages from both the MSC/VLR side as well
+ * as from the EUSE side */
+int rx_proc_ss_req(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup)
+{
+	struct hlr *hlr = conn->server->priv;
+	struct ss_session *ss;
+	struct ss_request req = {0};
+
+	LOGP(DSS, LOGL_DEBUG, "%s/0x%08x: Process SS (%s)\n", gsup->imsi, gsup->session_id,
+		osmo_gsup_session_state_name(gsup->session_state));
+
+	/* decode and find out what kind of SS message it is */
+	if (gsup->ss_info && gsup->ss_info_len) {
+		if (gsm0480_parse_facility_ie(gsup->ss_info, gsup->ss_info_len, &req)) {
+			LOGP(DSS, LOGL_ERROR, "%s/0x%082x: Unable to parse SS request: %s\n",
+				gsup->imsi, gsup->session_id,
+				osmo_hexdump(gsup->ss_info, gsup->ss_info_len));
+			/* FIXME: Send a Reject component? */
+			goto out_err;
+		}
+	}
+
+	switch (gsup->session_state) {
+	case OSMO_GSUP_SESSION_STATE_BEGIN:
+		/* Check for overlapping Session ID usage */
+		if (ss_session_find(hlr, gsup->imsi, gsup->session_id)) {
+			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: BEGIN with non-unique session ID!\n",
+				gsup->imsi, gsup->session_id);
+			goto out_err;
+		}
+		ss = ss_session_alloc(hlr, gsup->imsi, gsup->session_id);
+		if (!ss) {
+			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: Unable to allocate SS session\n",
+				gsup->imsi, gsup->session_id);
+			goto out_err;
+		}
+		if (ss_op_is_ussd(req.opcode)) {
+			if (conn_is_euse(conn)) {
+				/* EUSE->VLR: MT USSD. EUSE is known ('conn'), VLR is to be resolved */
+				ss->u.euse = euse_by_conn(conn);
+			} else {
+				/* VLR->EUSE: MO USSD. VLR is known ('conn'), EUSE is to be resolved */
+				struct hlr_ussd_route *rt;
+				rt = ussd_route_lookup_7bit(hlr, (const char *) req.ussd_text);
+				if (rt) {
+					if (rt->is_external) {
+						ss->is_external = true;
+						ss->u.euse = rt->u.euse;
+					} else if (rt) {
+						ss->is_external = false;
+						ss->u.iuse = rt->u.iuse;
+					}
+				} else {
+					if (hlr->euse_default) {
+						ss->is_external = true;
+						ss->u.euse = hlr->euse_default;
+					}
+				}
+			}
+			/* dispatch unstructured SS to routing */
+			handle_ussd(conn, ss, gsup, &req);
+		} else {
+			/* dispatch non-call SS to internal code */
+			handle_ss(ss, gsup, &req);
+		}
+		break;
+	case OSMO_GSUP_SESSION_STATE_CONTINUE:
+		ss = ss_session_find(hlr, gsup->imsi, gsup->session_id);
+		if (!ss) {
+			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: CONTINUE for unknown SS session\n",
+				gsup->imsi, gsup->session_id);
+			goto out_err;
+		}
+		if (ss_op_is_ussd(req.opcode)) {
+			/* dispatch unstructured SS to routing */
+			handle_ussd(conn, ss, gsup, &req);
+		} else {
+			/* dispatch non-call SS to internal code */
+			handle_ss(ss, gsup, &req);
+		}
+		break;
+	case OSMO_GSUP_SESSION_STATE_END:
+		ss = ss_session_find(hlr, gsup->imsi, gsup->session_id);
+		if (!ss) {
+			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: END for unknown SS session\n",
+				gsup->imsi, gsup->session_id);
+			goto out_err;
+		}
+		if (ss_op_is_ussd(req.opcode)) {
+			/* dispatch unstructured SS to routing */
+			handle_ussd(conn, ss, gsup, &req);
+		} else {
+			/* dispatch non-call SS to internal code */
+			handle_ss(ss, gsup, &req);
+		}
+		ss_session_free(ss);
+		break;
+	default:
+		LOGP(DSS, LOGL_ERROR, "%s/0x%08x: Unknown SS State %d\n", gsup->imsi,
+			gsup->session_id, gsup->session_state);
+		goto out_err;
+	}
+
+	return 0;
+
+out_err:
+	return 0;
+}
+
+int rx_proc_ss_error(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup)
+{
+	LOGP(DSS, LOGL_NOTICE, "%s/0x%08x: Process SS ERROR (%s)\n", gsup->imsi, gsup->session_id,
+		osmo_gsup_session_state_name(gsup->session_state));
+	return 0;
+}
diff --git a/src/hlr_ussd.h b/src/hlr_ussd.h
new file mode 100644
index 0000000..0414924
--- /dev/null
+++ b/src/hlr_ussd.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <stdbool.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/gsm/gsup.h>
+
+#include "gsup_server.h"
+
+struct hlr_ussd_route {
+	/* g_hlr.routes */
+	struct llist_head list;
+	const char *prefix;
+	bool is_external;
+	union {
+		struct hlr_euse *euse;
+		const struct hlr_iuse *iuse;
+	} u;
+};
+
+struct hlr_euse {
+	/* list in the per-hlr list of EUSEs */
+	struct llist_head list;
+	struct hlr *hlr;
+	/* name (must match the IPA ID tag) */
+	const char *name;
+	/* human-readable description */
+	const char *description;
+
+	/* GSUP connection to the EUSE, if any */
+	struct osmo_gsup_conn *conn;
+};
+
+struct hlr_euse *euse_find(struct hlr *hlr, const char *name);
+struct hlr_euse *euse_alloc(struct hlr *hlr, const char *name);
+void euse_del(struct hlr_euse *euse);
+
+const struct hlr_iuse *iuse_find(const char *name);
+
+struct hlr_ussd_route *ussd_route_find_prefix(struct hlr *hlr, const char *prefix);
+struct hlr_ussd_route *ussd_route_prefix_alloc_int(struct hlr *hlr, const char *prefix,
+						   const struct hlr_iuse *iuse);
+struct hlr_ussd_route *ussd_route_prefix_alloc_ext(struct hlr *hlr, const char *prefix,
+						   struct hlr_euse *euse);
+void ussd_route_del(struct hlr_ussd_route *rt);
+
+int rx_proc_ss_req(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup);
+int rx_proc_ss_error(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup);
+
+struct ss_session;
+struct ss_request;
+
+/* Internal USSD Handler */
+struct hlr_iuse {
+	const char *name;
+	/* call-back to be called for any incoming USSD messages for this IUSE */
+	int (*handle_ussd)(struct osmo_gsup_conn *conn, struct ss_session *ss,
+			   const struct osmo_gsup_message *gsup, const struct ss_request *req);
+};
diff --git a/src/hlr_vty.c b/src/hlr_vty.c
new file mode 100644
index 0000000..2d9b929
--- /dev/null
+++ b/src/hlr_vty.c
@@ -0,0 +1,358 @@
+/* OsmoHLR VTY implementation */
+
+/* (C) 2016 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ * (C) 2018 Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * (C) 2018 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 <osmocom/core/talloc.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/logging.h>
+#include <osmocom/vty/misc.h>
+#include <osmocom/abis/ipa.h>
+
+#include "hlr.h"
+#include "hlr_vty.h"
+#include "hlr_vty_subscr.h"
+#include "gsup_server.h"
+
+struct cmd_node hlr_node = {
+	HLR_NODE,
+	"%s(config-hlr)# ",
+	1,
+};
+
+DEFUN(cfg_hlr,
+      cfg_hlr_cmd,
+      "hlr",
+      "Configure the HLR")
+{
+	vty->node = HLR_NODE;
+	return CMD_SUCCESS;
+}
+
+struct cmd_node gsup_node = {
+	GSUP_NODE,
+	"%s(config-hlr-gsup)# ",
+	1,
+};
+
+DEFUN(cfg_gsup,
+      cfg_gsup_cmd,
+      "gsup",
+      "Configure GSUP options")
+{
+	vty->node = GSUP_NODE;
+	return CMD_SUCCESS;
+}
+
+static int config_write_hlr(struct vty *vty)
+{
+	vty_out(vty, "hlr%s", VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+static int config_write_hlr_gsup(struct vty *vty)
+{
+	vty_out(vty, " gsup%s", VTY_NEWLINE);
+	if (g_hlr->gsup_bind_addr)
+		vty_out(vty, "  bind ip %s%s", g_hlr->gsup_bind_addr, VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+static void show_one_conn(struct vty *vty, const struct osmo_gsup_conn *conn)
+{
+	const struct ipa_server_conn *isc = conn->conn;
+	char *name;
+	int rc;
+
+	rc = osmo_gsup_conn_ccm_get(conn, (uint8_t **) &name, IPAC_IDTAG_SERNR);
+	OSMO_ASSERT(rc);
+
+	vty_out(vty, " '%s' from %s:%5u, CS=%u, PS=%u, 3G_IND=%u%s",
+		name, isc->addr, isc->port, conn->supports_cs, conn->supports_ps, conn->auc_3g_ind,
+		VTY_NEWLINE);
+}
+
+DEFUN(show_gsup_conn, show_gsup_conn_cmd,
+	"show gsup-connections",
+	SHOW_STR "GSUP Connections from VLRs, SGSNs, EUSEs\n")
+{
+	struct osmo_gsup_server *gs = g_hlr->gs;
+	struct osmo_gsup_conn *conn;
+
+	llist_for_each_entry(conn, &gs->clients, list)
+		show_one_conn(vty, conn);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_hlr_gsup_bind_ip,
+      cfg_hlr_gsup_bind_ip_cmd,
+      "bind ip A.B.C.D",
+      "Listen/Bind related socket option\n"
+      IP_STR
+      "IPv4 Address to bind the GSUP interface to\n")
+{
+	if(g_hlr->gsup_bind_addr)
+		talloc_free(g_hlr->gsup_bind_addr);
+	g_hlr->gsup_bind_addr = talloc_strdup(g_hlr, argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+/***********************************************************************
+ * USSD Entity
+ ***********************************************************************/
+
+#include "hlr_ussd.h"
+
+#define USSD_STR "USSD Configuration\n"
+#define UROUTE_STR "Routing Configuration\n"
+#define PREFIX_STR "Prefix-Matching Route\n" "USSD Prefix\n"
+
+#define INT_CHOICE "(own-msisdn|own-imsi)"
+#define INT_STR "Internal USSD Handler\n" \
+		"Respond with subscribers' own MSISDN\n" \
+		"Respond with subscribers' own IMSI\n"
+
+#define EXT_STR "External USSD Handler\n" \
+		"Name of External USSD Handler (IPA CCM ID)\n"
+
+DEFUN(cfg_ussd_route_pfx_int, cfg_ussd_route_pfx_int_cmd,
+	"ussd route prefix PREFIX internal " INT_CHOICE,
+	USSD_STR UROUTE_STR PREFIX_STR INT_STR)
+{
+	const struct hlr_iuse *iuse = iuse_find(argv[1]);
+	struct hlr_ussd_route *rt = ussd_route_find_prefix(g_hlr, argv[0]);
+	if (rt) {
+		vty_out(vty, "%% Cannot add [another?] route for prefix %s%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	ussd_route_prefix_alloc_int(g_hlr, argv[0], iuse);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ussd_route_pfx_ext, cfg_ussd_route_pfx_ext_cmd,
+	"ussd route prefix PREFIX external EUSE",
+	USSD_STR UROUTE_STR PREFIX_STR EXT_STR)
+{
+	struct hlr_euse *euse = euse_find(g_hlr, argv[1]);
+	struct hlr_ussd_route *rt = ussd_route_find_prefix(g_hlr, argv[0]);
+	if (rt) {
+		vty_out(vty, "%% Cannot add [another?] route for prefix %s%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!euse) {
+		vty_out(vty, "%% Cannot find euse '%s'%s", argv[1], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	ussd_route_prefix_alloc_ext(g_hlr, argv[0], euse);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ussd_no_route_pfx, cfg_ussd_no_route_pfx_cmd,
+	"no ussd route prefix PREFIX",
+	NO_STR USSD_STR UROUTE_STR PREFIX_STR)
+{
+	struct hlr_ussd_route *rt = ussd_route_find_prefix(g_hlr, argv[0]);
+	if (!rt) {
+		vty_out(vty, "%% Cannot find route for prefix %s%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	ussd_route_del(rt);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ussd_defaultroute, cfg_ussd_defaultroute_cmd,
+	"ussd default-route external EUSE",
+	USSD_STR "Configure default-route for all USSD to unknown destinations\n"
+	EXT_STR)
+{
+	struct hlr_euse *euse;
+
+	euse = euse_find(g_hlr, argv[0]);
+	if (!euse) {
+		vty_out(vty, "%% Cannot find EUSE %s%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (g_hlr->euse_default != euse) {
+		vty_out(vty, "Switching default route from %s to %s%s",
+			g_hlr->euse_default ? g_hlr->euse_default->name : "<none>",
+			euse->name, VTY_NEWLINE);
+		g_hlr->euse_default = euse;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ussd_no_defaultroute, cfg_ussd_no_defaultroute_cmd,
+	"no ussd default-route",
+	NO_STR USSD_STR "Remove the default-route for all USSD to unknown destinations\n")
+{
+	g_hlr->euse_default = NULL;
+
+	return CMD_SUCCESS;
+}
+
+struct cmd_node euse_node = {
+	EUSE_NODE,
+	"%s(config-hlr-euse)# ",
+	1,
+};
+
+DEFUN(cfg_euse, cfg_euse_cmd,
+	"euse NAME",
+	"Configure a particular External USSD Entity\n"
+	"Alphanumeric name of the External USSD Entity\n")
+{
+	struct hlr_euse *euse;
+	const char *id = argv[0];
+
+	euse = euse_find(g_hlr, id);
+	if (!euse) {
+		euse = euse_alloc(g_hlr, id);
+		if (!euse)
+			return CMD_WARNING;
+	}
+	vty->index = euse;
+	vty->index_sub = &euse->description;
+	vty->node = EUSE_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_euse, cfg_no_euse_cmd,
+	"no euse NAME",
+	NO_STR "Remove a particular External USSD Entity\n"
+	"Alphanumeric name of the External USSD Entity\n")
+{
+	struct hlr_euse *euse = euse_find(g_hlr, argv[0]);
+	if (!euse) {
+		vty_out(vty, "%% Cannot remove non-existant EUSE %s%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (g_hlr->euse_default == euse) {
+		vty_out(vty, "%% Cannot remove EUSE %s, it is the default route%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	euse_del(euse);
+	return CMD_SUCCESS;
+}
+
+static void dump_one_euse(struct vty *vty, struct hlr_euse *euse)
+{
+	vty_out(vty, " euse %s%s", euse->name, VTY_NEWLINE);
+}
+
+static int config_write_euse(struct vty *vty)
+{
+	struct hlr_euse *euse;
+	struct hlr_ussd_route *rt;
+
+	llist_for_each_entry(euse, &g_hlr->euse_list, list)
+		dump_one_euse(vty, euse);
+
+	llist_for_each_entry(rt, &g_hlr->ussd_routes, list) {
+		vty_out(vty, " ussd route prefix %s %s %s%s", rt->prefix,
+			rt->is_external ? "external" : "internal",
+			rt->is_external ? rt->u.euse->name : rt->u.iuse->name,
+			VTY_NEWLINE);
+	}
+
+	if (g_hlr->euse_default)
+		vty_out(vty, " ussd default-route external %s%s", g_hlr->euse_default->name, VTY_NEWLINE);
+
+	return 0;
+}
+
+/***********************************************************************
+ * Common Code
+ ***********************************************************************/
+
+int hlr_vty_go_parent(struct vty *vty)
+{
+	switch (vty->node) {
+	case GSUP_NODE:
+	case EUSE_NODE:
+		vty->node = HLR_NODE;
+		vty->index = NULL;
+		vty->index_sub = NULL;
+		break;
+	default:
+	case HLR_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		break;
+	case CONFIG_NODE:
+		vty->node = ENABLE_NODE;
+		vty->index = NULL;
+		break;
+	}
+
+	return vty->node;
+}
+
+int hlr_vty_is_config_node(struct vty *vty, int node)
+{
+	switch (node) {
+	/* add items that are not config */
+	case CONFIG_NODE:
+		return 0;
+
+	default:
+		return 1;
+	}
+}
+
+void hlr_vty_init(const struct log_info *cat)
+{
+	logging_vty_add_cmds(cat);
+	osmo_talloc_vty_add_cmds();
+
+	install_element_ve(&show_gsup_conn_cmd);
+
+	install_element(CONFIG_NODE, &cfg_hlr_cmd);
+	install_node(&hlr_node, config_write_hlr);
+
+	install_element(HLR_NODE, &cfg_gsup_cmd);
+	install_node(&gsup_node, config_write_hlr_gsup);
+
+	install_element(GSUP_NODE, &cfg_hlr_gsup_bind_ip_cmd);
+
+	install_element(HLR_NODE, &cfg_euse_cmd);
+	install_element(HLR_NODE, &cfg_no_euse_cmd);
+	install_node(&euse_node, config_write_euse);
+	install_element(HLR_NODE, &cfg_ussd_route_pfx_int_cmd);
+	install_element(HLR_NODE, &cfg_ussd_route_pfx_ext_cmd);
+	install_element(HLR_NODE, &cfg_ussd_no_route_pfx_cmd);
+	install_element(HLR_NODE, &cfg_ussd_defaultroute_cmd);
+	install_element(HLR_NODE, &cfg_ussd_no_defaultroute_cmd);
+
+	hlr_vty_subscriber_init();
+}
diff --git a/src/hlr_vty.h b/src/hlr_vty.h
new file mode 100644
index 0000000..1f3b87f
--- /dev/null
+++ b/src/hlr_vty.h
@@ -0,0 +1,38 @@
+/* OsmoHLR VTY implementation */
+
+/* (C) 2016 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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/>.
+ *
+ */
+
+#pragma once
+
+#include <osmocom/core/logging.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include "hlr.h"
+
+enum hlr_vty_node {
+	HLR_NODE = _LAST_OSMOVTY_NODE + 1,
+	GSUP_NODE,
+	EUSE_NODE,
+};
+
+int hlr_vty_is_config_node(struct vty *vty, int node);
+int hlr_vty_go_parent(struct vty *vty);
+void hlr_vty_init(const struct log_info *cat);
diff --git a/src/hlr_vty_subscr.c b/src/hlr_vty_subscr.c
new file mode 100644
index 0000000..6ce96fe
--- /dev/null
+++ b/src/hlr_vty_subscr.c
@@ -0,0 +1,488 @@
+/* OsmoHLR subscriber management VTY implementation */
+/* (C) 2017 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * 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 <inttypes.h>
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/gsm/gsm23003.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/core/utils.h>
+
+#include "hlr.h"
+#include "db.h"
+
+struct vty;
+
+#define hexdump_buf(buf) osmo_hexdump_nospc((void*)buf, sizeof(buf))
+
+static void subscr_dump_full_vty(struct vty *vty, struct hlr_subscriber *subscr)
+{
+	int rc;
+	struct osmo_sub_auth_data aud2g;
+	struct osmo_sub_auth_data aud3g;
+
+	vty_out(vty, "    ID: %"PRIu64"%s", subscr->id, VTY_NEWLINE);
+
+	vty_out(vty, "    IMSI: %s%s", *subscr->imsi ? subscr->imsi : "none", VTY_NEWLINE);
+	vty_out(vty, "    MSISDN: %s%s", *subscr->msisdn ? subscr->msisdn : "none", VTY_NEWLINE);
+	if (*subscr->vlr_number)
+		vty_out(vty, "    VLR number: %s%s", subscr->vlr_number, VTY_NEWLINE);
+	if (*subscr->sgsn_number)
+		vty_out(vty, "    SGSN number: %s%s", subscr->sgsn_number, VTY_NEWLINE);
+	if (*subscr->sgsn_address)
+		vty_out(vty, "    SGSN address: %s%s", subscr->sgsn_address, VTY_NEWLINE);
+	if (subscr->periodic_lu_timer)
+		vty_out(vty, "    Periodic LU timer: %u%s", subscr->periodic_lu_timer, VTY_NEWLINE);
+	if (subscr->periodic_rau_tau_timer)
+		vty_out(vty, "    Periodic RAU/TAU timer: %u%s", subscr->periodic_rau_tau_timer, VTY_NEWLINE);
+	if (subscr->lmsi)
+		vty_out(vty, "    LMSI: %x%s", subscr->lmsi, VTY_NEWLINE);
+	if (!subscr->nam_cs)
+		vty_out(vty, "    CS disabled%s", VTY_NEWLINE);
+	if (subscr->ms_purged_cs)
+		vty_out(vty, "    CS purged%s", VTY_NEWLINE);
+	if (!subscr->nam_ps)
+		vty_out(vty, "    PS disabled%s", VTY_NEWLINE);
+	if (subscr->ms_purged_ps)
+		vty_out(vty, "    PS purged%s", VTY_NEWLINE);
+
+	if (!*subscr->imsi)
+		return;
+
+	OSMO_ASSERT(g_hlr);
+	rc = db_get_auth_data(g_hlr->dbc, subscr->imsi, &aud2g, &aud3g, NULL);
+
+	switch (rc) {
+	case 0:
+		break;
+	case -ENOENT:
+	case -ENOKEY:
+		aud2g.algo = OSMO_AUTH_ALG_NONE;
+		aud3g.algo = OSMO_AUTH_ALG_NONE;
+		break;
+	default:
+		vty_out(vty, "%% Error retrieving data from database (%d)%s", rc, VTY_NEWLINE);
+		return;
+	}
+
+	if (aud2g.type != OSMO_AUTH_TYPE_NONE && aud2g.type != OSMO_AUTH_TYPE_GSM) {
+		vty_out(vty, "%% Error: 2G auth data is not of type 'GSM'%s", VTY_NEWLINE);
+		aud2g = (struct osmo_sub_auth_data){};
+	}
+
+	if (aud3g.type != OSMO_AUTH_TYPE_NONE && aud3g.type != OSMO_AUTH_TYPE_UMTS) {
+		vty_out(vty, "%% Error: 3G auth data is not of type 'UMTS'%s", VTY_NEWLINE);
+		aud3g = (struct osmo_sub_auth_data){};
+	}
+
+	if (aud2g.algo != OSMO_AUTH_ALG_NONE && aud2g.type != OSMO_AUTH_TYPE_NONE) {
+		vty_out(vty, "    2G auth: %s%s",
+			osmo_auth_alg_name(aud2g.algo), VTY_NEWLINE);
+		vty_out(vty, "             KI=%s%s",
+			hexdump_buf(aud2g.u.gsm.ki), VTY_NEWLINE);
+	}
+
+	if (aud3g.algo != OSMO_AUTH_ALG_NONE && aud3g.type != OSMO_AUTH_TYPE_NONE) {
+		vty_out(vty, "    3G auth: %s%s", osmo_auth_alg_name(aud3g.algo), VTY_NEWLINE);
+		vty_out(vty, "             K=%s%s", hexdump_buf(aud3g.u.umts.k), VTY_NEWLINE);
+		vty_out(vty, "             %s=%s%s", aud3g.u.umts.opc_is_op? "OP" : "OPC",
+			hexdump_buf(aud3g.u.umts.opc), VTY_NEWLINE);
+		vty_out(vty, "             IND-bitlen=%u", aud3g.u.umts.ind_bitlen);
+		if (aud3g.u.umts.sqn)
+			vty_out(vty, " last-SQN=%"PRIu64, aud3g.u.umts.sqn);
+		vty_out(vty, VTY_NEWLINE);
+	}
+}
+
+static int get_subscr_by_argv(struct vty *vty, const char *type, const char *id, struct hlr_subscriber *subscr)
+{
+	int rc = -1;
+	if (strcmp(type, "imsi") == 0)
+		rc = db_subscr_get_by_imsi(g_hlr->dbc, id, subscr);
+	else if (strcmp(type, "msisdn") == 0)
+		rc = db_subscr_get_by_msisdn(g_hlr->dbc, id, subscr);
+	else if (strcmp(type, "id") == 0)
+		rc = db_subscr_get_by_id(g_hlr->dbc, atoll(id), subscr);
+	if (rc)
+		vty_out(vty, "%% No subscriber for %s = '%s'%s",
+			type, id, VTY_NEWLINE);
+	return rc;
+}
+
+#define SUBSCR_CMD "subscriber "
+#define SUBSCR_CMD_HELP "Subscriber management commands\n"
+
+#define SUBSCR_ID "(imsi|msisdn|id) IDENT "
+#define SUBSCR_ID_HELP \
+	"Identify subscriber by IMSI\n" \
+	"Identify subscriber by MSISDN (phone number)\n" \
+	"Identify subscriber by database ID\n" \
+	"IMSI/MSISDN/ID of the subscriber\n"
+
+#define SUBSCR 		SUBSCR_CMD SUBSCR_ID
+#define SUBSCR_HELP	SUBSCR_CMD_HELP SUBSCR_ID_HELP
+
+#define SUBSCR_UPDATE		SUBSCR "update "
+#define SUBSCR_UPDATE_HELP	SUBSCR_HELP "Set or update subscriber data\n"
+
+DEFUN(subscriber_show,
+      subscriber_show_cmd,
+      SUBSCR "show",
+      SUBSCR_HELP "Show subscriber information\n")
+{
+	struct hlr_subscriber subscr;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	subscr_dump_full_vty(vty, &subscr);
+	return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_create,
+      subscriber_create_cmd,
+      SUBSCR_CMD "imsi IDENT create",
+      SUBSCR_CMD_HELP
+      "Identify subscriber by IMSI\n"
+      "IMSI/MSISDN/ID of the subscriber\n"
+      "Create subscriber by IMSI\n")
+{
+	int rc;
+	struct hlr_subscriber subscr;
+	const char *imsi = argv[0];
+	
+	if (!osmo_imsi_str_valid(imsi)) {
+		vty_out(vty, "%% Not a valid IMSI: %s%s", imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	rc = db_subscr_create(g_hlr->dbc, imsi);
+
+	if (rc) {
+		if (rc == -EEXIST)
+			vty_out(vty, "%% Subscriber already exists for IMSI = %s%s",
+				imsi, VTY_NEWLINE);
+		else
+			vty_out(vty, "%% Error (rc=%d): cannot create subscriber for IMSI = %s%s",
+				rc, imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	rc = db_subscr_get_by_imsi(g_hlr->dbc, imsi, &subscr);
+	vty_out(vty, "%% Created subscriber %s%s", imsi, VTY_NEWLINE);
+
+	subscr_dump_full_vty(vty, &subscr);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_delete,
+      subscriber_delete_cmd,
+      SUBSCR "delete",
+      SUBSCR_HELP "Delete subscriber from database\n")
+{
+	struct hlr_subscriber subscr;
+	int rc;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+
+	/* Find out the IMSI regardless of which way the caller decided to
+	 * identify the subscriber by. */
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	rc = db_subscr_delete_by_id(g_hlr->dbc, subscr.id);
+	if (rc) {
+		vty_out(vty, "%% Error: Failed to remove subscriber for IMSI '%s'%s",
+			subscr.imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	vty_out(vty, "%% Deleted subscriber for IMSI '%s'%s", subscr.imsi, VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_msisdn,
+      subscriber_msisdn_cmd,
+      SUBSCR_UPDATE "msisdn MSISDN",
+      SUBSCR_UPDATE_HELP
+      "Set MSISDN (phone number) of the subscriber\n"
+      "New MSISDN (phone number)\n")
+{
+	struct hlr_subscriber subscr;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+	const char *msisdn = argv[2];
+
+	if (strlen(msisdn) > sizeof(subscr.msisdn) - 1) {
+		vty_out(vty, "%% MSISDN is too long, max. %zu characters are allowed%s",
+			sizeof(subscr.msisdn)-1, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!osmo_msisdn_str_valid(msisdn)) {
+		vty_out(vty, "%% MSISDN invalid: '%s'%s", msisdn, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	if (db_subscr_update_msisdn_by_imsi(g_hlr->dbc, subscr.imsi, msisdn)) {
+		vty_out(vty, "%% Error: cannot update MSISDN for subscriber IMSI='%s'%s",
+			subscr.imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	vty_out(vty, "%% Updated subscriber IMSI='%s' to MSISDN='%s'%s",
+		subscr.imsi, msisdn, VTY_NEWLINE);
+
+	if (db_subscr_get_by_msisdn(g_hlr->dbc, msisdn, &subscr) == 0)
+		osmo_hlr_subscriber_update_notify(&subscr);
+
+	return CMD_SUCCESS;
+}
+
+static bool is_hexkey_valid(struct vty *vty, const char *label,
+			    const char *hex_str, int minlen, int maxlen)
+{
+	if (osmo_is_hexstr(hex_str, minlen * 2, maxlen * 2, true))
+		return true;
+	vty_out(vty, "%% Invalid value for %s: '%s'%s", label, hex_str, VTY_NEWLINE);
+	return false;
+}
+
+#define AUTH_ALG_TYPES_2G "(comp128v1|comp128v2|comp128v3|xor)"
+#define AUTH_ALG_TYPES_2G_HELP \
+	"Use COMP128v1 algorithm\n" \
+	"Use COMP128v2 algorithm\n" \
+	"Use COMP128v3 algorithm\n" \
+	"Use XOR algorithm\n"
+
+#define AUTH_ALG_TYPES_3G "milenage"
+#define AUTH_ALG_TYPES_3G_HELP \
+	"Use Milenage algorithm\n"
+
+#define A38_XOR_MIN_KEY_LEN	12
+#define A38_XOR_MAX_KEY_LEN	16
+#define A38_COMP128_KEY_LEN	16
+
+#define MILENAGE_KEY_LEN 16
+
+static bool auth_algo_parse(const char *alg_str, enum osmo_auth_algo *algo,
+			    int *minlen, int *maxlen)
+{
+	if (!strcasecmp(alg_str, "none")) {
+		*algo = OSMO_AUTH_ALG_NONE;
+		*minlen = *maxlen = 0;
+	} else if (!strcasecmp(alg_str, "comp128v1")) {
+		*algo = OSMO_AUTH_ALG_COMP128v1;
+		*minlen = *maxlen = A38_COMP128_KEY_LEN;
+	} else if (!strcasecmp(alg_str, "comp128v2")) {
+		*algo = OSMO_AUTH_ALG_COMP128v2;
+		*minlen = *maxlen = A38_COMP128_KEY_LEN;
+	} else if (!strcasecmp(alg_str, "comp128v3")) {
+		*algo = OSMO_AUTH_ALG_COMP128v3;
+		*minlen = *maxlen = A38_COMP128_KEY_LEN;
+	} else if (!strcasecmp(alg_str, "xor")) {
+		*algo = OSMO_AUTH_ALG_XOR;
+		*minlen = A38_XOR_MIN_KEY_LEN;
+		*maxlen = A38_XOR_MAX_KEY_LEN;
+	} else if (!strcasecmp(alg_str, "milenage")) {
+		*algo = OSMO_AUTH_ALG_MILENAGE;
+		*minlen = *maxlen = MILENAGE_KEY_LEN;
+	} else
+		return false;
+	return true;
+}
+
+DEFUN(subscriber_no_aud2g,
+      subscriber_no_aud2g_cmd,
+      SUBSCR_UPDATE "aud2g none",
+      SUBSCR_UPDATE_HELP
+      "Set 2G authentication data\n"
+      "Delete 2G authentication data\n")
+{
+	struct hlr_subscriber subscr;
+	int rc;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+	struct sub_auth_data_str aud = {
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud);
+
+	if (rc && rc != -ENOENT) {
+		vty_out(vty, "%% Error: cannot disable 2G auth data for IMSI='%s'%s",
+			subscr.imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_aud2g,
+      subscriber_aud2g_cmd,
+      SUBSCR_UPDATE "aud2g " AUTH_ALG_TYPES_2G " ki KI",
+      SUBSCR_UPDATE_HELP
+      "Set 2G authentication data\n"
+      AUTH_ALG_TYPES_2G_HELP
+      "Set Ki Encryption Key\n" "Ki as 32 hexadecimal characters\n")
+{
+	struct hlr_subscriber subscr;
+	int rc;
+	int minlen = 0;
+	int maxlen = 0;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+	const char *alg_type = argv[2];
+	const char *ki = argv[3];
+	struct sub_auth_data_str aud2g = {
+		.type = OSMO_AUTH_TYPE_GSM,
+		.u.gsm.ki = ki,
+	};
+
+	if (!auth_algo_parse(alg_type, &aud2g.algo, &minlen, &maxlen)) {
+		vty_out(vty, "%% Unknown auth algorithm: '%s'%s", alg_type, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!is_hexkey_valid(vty, "KI", aud2g.u.gsm.ki, minlen, maxlen))
+		return CMD_WARNING;
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud2g);
+
+	if (rc) {
+		vty_out(vty, "%% Error: cannot set 2G auth data for IMSI='%s'%s",
+			subscr.imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_no_aud3g,
+      subscriber_no_aud3g_cmd,
+      SUBSCR_UPDATE "aud3g none",
+      SUBSCR_UPDATE_HELP
+      "Set UMTS authentication data (3G, and 2G with UMTS AKA)\n"
+      "Delete 3G authentication data\n")
+{
+	struct hlr_subscriber subscr;
+	int rc;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+	struct sub_auth_data_str aud = {
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud);
+
+	if (rc && rc != -ENOENT) {
+		vty_out(vty, "%% Error: cannot disable 3G auth data for IMSI='%s'%s",
+			subscr.imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_aud3g,
+      subscriber_aud3g_cmd,
+      SUBSCR_UPDATE "aud3g " AUTH_ALG_TYPES_3G
+      " k K"
+      " (op|opc) OP_C"
+      " [ind-bitlen] [<0-28>]",
+      SUBSCR_UPDATE_HELP
+      "Set UMTS authentication data (3G, and 2G with UMTS AKA)\n"
+      AUTH_ALG_TYPES_3G_HELP
+      "Set Encryption Key K\n" "K as 32 hexadecimal characters\n"
+      "Set OP key\n" "Set OPC key\n" "OP or OPC as 32 hexadecimal characters\n"
+      "Set IND bit length\n" "IND bit length value (default: 5)\n")
+{
+	struct hlr_subscriber subscr;
+	int minlen = 0;
+	int maxlen = 0;
+	int rc;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+	const char *alg_type = AUTH_ALG_TYPES_3G;
+	const char *k = argv[2];
+	bool opc_is_op = (strcasecmp("op", argv[3]) == 0);
+	const char *op_opc = argv[4];
+	int ind_bitlen = argc > 6? atoi(argv[6]) : 5;
+	struct sub_auth_data_str aud3g = {
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.u.umts = {
+			.k = k,
+			.opc_is_op = opc_is_op,
+			.opc = op_opc,
+			.ind_bitlen = ind_bitlen,
+		},
+	};
+	
+	if (!auth_algo_parse(alg_type, &aud3g.algo, &minlen, &maxlen)) {
+		vty_out(vty, "%% Unknown auth algorithm: '%s'%s", alg_type, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!is_hexkey_valid(vty, "K", aud3g.u.umts.k, minlen, maxlen))
+		return CMD_WARNING;
+
+	if (!is_hexkey_valid(vty, opc_is_op ? "OP" : "OPC", aud3g.u.umts.opc,
+			     MILENAGE_KEY_LEN, MILENAGE_KEY_LEN))
+		return CMD_WARNING;
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud3g);
+
+	if (rc) {
+		vty_out(vty, "%% Error: cannot set 3G auth data for IMSI='%s'%s",
+			subscr.imsi, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+void hlr_vty_subscriber_init(void)
+{
+	install_element_ve(&subscriber_show_cmd);
+	install_element(ENABLE_NODE, &subscriber_create_cmd);
+	install_element(ENABLE_NODE, &subscriber_delete_cmd);
+	install_element(ENABLE_NODE, &subscriber_msisdn_cmd);
+	install_element(ENABLE_NODE, &subscriber_no_aud2g_cmd);
+	install_element(ENABLE_NODE, &subscriber_aud2g_cmd);
+	install_element(ENABLE_NODE, &subscriber_no_aud3g_cmd);
+	install_element(ENABLE_NODE, &subscriber_aud3g_cmd);
+}
diff --git a/src/hlr_vty_subscr.h b/src/hlr_vty_subscr.h
new file mode 100644
index 0000000..5dd0772
--- /dev/null
+++ b/src/hlr_vty_subscr.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void hlr_vty_subscriber_init(void);
diff --git a/src/logging.c b/src/logging.c
new file mode 100644
index 0000000..3fa2a69
--- /dev/null
+++ b/src/logging.c
@@ -0,0 +1,34 @@
+#include <osmocom/core/utils.h>
+#include "logging.h"
+
+const struct log_info_cat hlr_log_info_cat[] = {
+	[DMAIN] = {
+		.name = "DMAIN",
+		.description = "Main Program",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+	[DDB] = {
+		.name = "DDB",
+		.description = "Database Layer",
+		.color = "\033[1;31m",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+	[DAUC] = {
+		.name = "DAUC",
+		.description = "Authentication Center",
+		.color = "\033[1;33m",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+	[DSS] = {
+		.name = "DSS",
+		.description = "Supplementary Services",
+		.color = "\033[1;34m",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+
+};
+
+const struct log_info hlr_log_info = {
+	.cat = hlr_log_info_cat,
+	.num_cat = ARRAY_SIZE(hlr_log_info_cat),
+};
diff --git a/src/logging.h b/src/logging.h
new file mode 100644
index 0000000..ed24075
--- /dev/null
+++ b/src/logging.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <osmocom/core/logging.h>
+
+enum {
+	DMAIN,
+	DDB,
+	DGSUP,
+	DAUC,
+	DSS,
+};
+
+extern const struct log_info hlr_log_info;
diff --git a/src/luop.c b/src/luop.c
new file mode 100644
index 0000000..aff4d81
--- /dev/null
+++ b/src/luop.c
@@ -0,0 +1,259 @@
+/* OsmoHLR TX/RX lu operations */
+
+/* (C) 2017 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Harald Welte <laforge@gnumonks.org>
+ *
+ * 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 <stdbool.h>
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsm/apn.h>
+
+#include "gsup_server.h"
+#include "gsup_router.h"
+#include "logging.h"
+#include "luop.h"
+
+const struct value_string lu_state_names[] = {
+	{ LU_S_NULL,			"NULL" },
+	{ LU_S_LU_RECEIVED,		"LU RECEIVED" },
+	{ LU_S_CANCEL_SENT,		"CANCEL SENT" },
+	{ LU_S_CANCEL_ACK_RECEIVED,	"CANCEL-ACK RECEIVED" },
+	{ LU_S_ISD_SENT,		"ISD SENT" },
+	{ LU_S_ISD_ACK_RECEIVED,	"ISD-ACK RECEIVED" },
+	{ LU_S_COMPLETE,		"COMPLETE" },
+	{ 0, NULL }
+};
+
+/* Transmit a given GSUP message for the given LU operation */
+static void _luop_tx_gsup(struct lu_operation *luop,
+			  const struct osmo_gsup_message *gsup)
+{
+	struct msgb *msg_out;
+
+	msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP LUOP");
+	OSMO_ASSERT(msg_out);
+	osmo_gsup_encode(msg_out, gsup);
+
+	osmo_gsup_addr_send(luop->gsup_server, luop->peer,
+			    talloc_total_size(luop->peer),
+			    msg_out);
+}
+
+static inline void fill_gsup_msg(struct osmo_gsup_message *out,
+				 const struct lu_operation *lu,
+				 enum osmo_gsup_message_type mt)
+{
+	memset(out, 0, sizeof(struct osmo_gsup_message));
+	if (lu)
+		osmo_strlcpy(out->imsi, lu->subscr.imsi,
+			     GSM23003_IMSI_MAX_DIGITS + 1);
+	out->message_type = mt;
+}
+
+/* timer call-back in case LU operation doesn't receive an response */
+static void lu_op_timer_cb(void *data)
+{
+	struct lu_operation *luop = data;
+
+	DEBUGP(DMAIN, "LU OP timer expired in state %s\n",
+		get_value_string(lu_state_names, luop->state));
+
+	switch (luop->state) {
+	case LU_S_CANCEL_SENT:
+		break;
+	case LU_S_ISD_SENT:
+		break;
+	default:
+		break;
+	}
+
+	lu_op_tx_error(luop, GMM_CAUSE_NET_FAIL);
+}
+
+bool lu_op_fill_subscr(struct lu_operation *luop, struct db_context *dbc,
+		       const char *imsi)
+{
+	struct hlr_subscriber *subscr = &luop->subscr;
+
+	if (db_subscr_get_by_imsi(dbc, imsi, subscr) < 0)
+		return false;
+
+	return true;
+}
+
+struct lu_operation *lu_op_alloc(struct osmo_gsup_server *srv)
+{
+	struct lu_operation *luop;
+
+	luop = talloc_zero(srv, struct lu_operation);
+	OSMO_ASSERT(luop);
+	luop->gsup_server = srv;
+	osmo_timer_setup(&luop->timer, lu_op_timer_cb, luop);
+
+	return luop;
+}
+
+void lu_op_free(struct lu_operation *luop)
+{
+	/* Only attempt to remove when it was ever added to a list. */
+	if (luop->list.next)
+		llist_del(&luop->list);
+
+	/* Delete timer just in case it is still pending. */
+	osmo_timer_del(&luop->timer);
+
+	talloc_free(luop);
+}
+
+struct lu_operation *lu_op_alloc_conn(struct osmo_gsup_conn *conn)
+{
+	uint8_t *peer_addr;
+	struct lu_operation *luop = lu_op_alloc(conn->server);
+	int rc = osmo_gsup_conn_ccm_get(conn, &peer_addr, IPAC_IDTAG_SERNR);
+	if (rc < 0) {
+		lu_op_free(luop);
+		return NULL;
+	}
+
+	luop->peer = talloc_memdup(luop, peer_addr, rc);
+
+	return luop;
+}
+
+/* FIXME: this doesn't seem to work at all */
+struct lu_operation *lu_op_by_imsi(const char *imsi,
+				   const struct llist_head *lst)
+{
+	struct lu_operation *luop;
+
+	llist_for_each_entry(luop, lst, list) {
+		if (!strcmp(imsi, luop->subscr.imsi))
+			return luop;
+	}
+	return NULL;
+}
+
+void lu_op_statechg(struct lu_operation *luop, enum lu_state new_state)
+{
+	enum lu_state old_state = luop->state;
+
+	DEBUGP(DMAIN, "LU OP state change: %s -> ",
+		get_value_string(lu_state_names, old_state));
+	DEBUGPC(DMAIN, "%s\n",
+		get_value_string(lu_state_names, new_state));
+
+	luop->state = new_state;
+}
+
+/*! Transmit UPD_LOC_ERROR and destroy lu_operation */
+void lu_op_tx_error(struct lu_operation *luop, enum gsm48_gmm_cause cause)
+{
+	struct osmo_gsup_message gsup;
+
+	DEBUGP(DMAIN, "%s: LU OP Tx Error (cause %s)\n",
+	       luop->subscr.imsi, get_value_string(gsm48_gmm_cause_names,
+						   cause));
+
+	fill_gsup_msg(&gsup, luop, OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR);
+	gsup.cause = cause;
+
+	_luop_tx_gsup(luop, &gsup);
+
+	lu_op_free(luop);
+}
+
+/*! Transmit UPD_LOC_RESULT and destroy lu_operation */
+void lu_op_tx_ack(struct lu_operation *luop)
+{
+	struct osmo_gsup_message gsup;
+
+	fill_gsup_msg(&gsup, luop, OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT);
+	//FIXME gsup.hlr_enc;
+
+	_luop_tx_gsup(luop, &gsup);
+
+	lu_op_free(luop);
+}
+
+/*! Send Cancel Location to old VLR/SGSN */
+void lu_op_tx_cancel_old(struct lu_operation *luop)
+{
+	struct osmo_gsup_message gsup;
+
+	OSMO_ASSERT(luop->state == LU_S_LU_RECEIVED);
+
+	fill_gsup_msg(&gsup, NULL, OSMO_GSUP_MSGT_LOCATION_CANCEL_REQUEST);
+	//gsup.cause = FIXME;
+	//gsup.cancel_type = FIXME;
+
+	_luop_tx_gsup(luop, &gsup);
+
+	lu_op_statechg(luop, LU_S_CANCEL_SENT);
+	osmo_timer_schedule(&luop->timer, CANCEL_TIMEOUT_SECS, 0);
+}
+
+/*! Transmit Insert Subscriber Data to new VLR/SGSN */
+void lu_op_tx_insert_subscr_data(struct lu_operation *luop)
+{
+	struct hlr_subscriber *subscr = &luop->subscr;
+	struct osmo_gsup_message gsup = { };
+	uint8_t msisdn_enc[OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN];
+	uint8_t apn[APN_MAXLEN];
+	enum osmo_gsup_cn_domain cn_domain;
+
+	OSMO_ASSERT(luop->state == LU_S_LU_RECEIVED ||
+		    luop->state == LU_S_CANCEL_ACK_RECEIVED);
+
+	if (luop->is_ps)
+		cn_domain = OSMO_GSUP_CN_DOMAIN_PS;
+	else
+		cn_domain = OSMO_GSUP_CN_DOMAIN_CS;
+
+	if (osmo_gsup_create_insert_subscriber_data_msg(&gsup, subscr->imsi, subscr->msisdn, msisdn_enc,
+							sizeof(msisdn_enc), apn, sizeof(apn), cn_domain) != 0) {
+		LOGP(DMAIN, LOGL_ERROR,
+		       "IMSI='%s': Cannot notify GSUP client; could not create gsup message "
+		       "for %s\n", subscr->imsi, luop->peer);
+		return;
+	}
+
+	/* Send ISD to new VLR/SGSN */
+	_luop_tx_gsup(luop, &gsup);
+
+	lu_op_statechg(luop, LU_S_ISD_SENT);
+	osmo_timer_schedule(&luop->timer, ISD_TIMEOUT_SECS, 0);
+}
+
+/*! Transmit Delete Subscriber Data to new VLR/SGSN.
+ * The luop is not freed. */
+void lu_op_tx_del_subscr_data(struct lu_operation *luop)
+{
+	struct osmo_gsup_message gsup;
+
+	fill_gsup_msg(&gsup, luop, OSMO_GSUP_MSGT_DELETE_DATA_REQUEST);
+
+	gsup.cn_domain = OSMO_GSUP_CN_DOMAIN_PS;
+
+	/* Send ISD to new VLR/SGSN */
+	_luop_tx_gsup(luop, &gsup);
+}
diff --git a/src/luop.h b/src/luop.h
new file mode 100644
index 0000000..f96593b
--- /dev/null
+++ b/src/luop.h
@@ -0,0 +1,81 @@
+/* OsmoHLR TX/RX lu operations */
+
+/* (C) 2017 sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Harald Welte <laforge@gnumonks.org>
+ *
+ * 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/>.
+ *
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include <osmocom/core/timer.h>
+#include <osmocom/gsm/gsup.h>
+
+#include "db.h"
+#include "gsup_server.h"
+
+#define CANCEL_TIMEOUT_SECS	30
+#define ISD_TIMEOUT_SECS	30
+
+enum lu_state {
+	LU_S_NULL,
+	LU_S_LU_RECEIVED,
+	LU_S_CANCEL_SENT,
+	LU_S_CANCEL_ACK_RECEIVED,
+	LU_S_ISD_SENT,
+	LU_S_ISD_ACK_RECEIVED,
+	LU_S_COMPLETE,
+};
+
+extern const struct value_string lu_state_names[];
+
+struct lu_operation {
+	/*! entry in global list of location update operations */
+	struct llist_head list;
+	/*! to which gsup_server do we belong */
+	struct osmo_gsup_server *gsup_server;
+	/*! state of the location update */
+	enum lu_state state;
+	/*! CS (false) or PS (true) Location Update? */
+	bool is_ps;
+	/*! currently running timer */
+	struct osmo_timer_list timer;
+
+	/*! subscriber related to this operation */
+	struct hlr_subscriber subscr;
+	/*! peer VLR/SGSN starting the request */
+	uint8_t *peer;
+};
+
+
+struct lu_operation *lu_op_alloc(struct osmo_gsup_server *srv);
+struct lu_operation *lu_op_alloc_conn(struct osmo_gsup_conn *conn);
+void lu_op_statechg(struct lu_operation *luop, enum lu_state new_state);
+bool lu_op_fill_subscr(struct lu_operation *luop, struct db_context *dbc,
+		       const char *imsi);
+struct lu_operation *lu_op_by_imsi(const char *imsi,
+				   const struct llist_head *lst);
+
+void lu_op_tx_error(struct lu_operation *luop, enum gsm48_gmm_cause cause);
+void lu_op_tx_ack(struct lu_operation *luop);
+void lu_op_tx_cancel_old(struct lu_operation *luop);
+void lu_op_tx_insert_subscr_data(struct lu_operation *luop);
+void lu_op_tx_del_subscr_data(struct lu_operation *luop);
+
+void lu_op_free(struct lu_operation *luop);
diff --git a/src/osmo-euse-demo.c b/src/osmo-euse-demo.c
new file mode 100644
index 0000000..4e4ef78
--- /dev/null
+++ b/src/osmo-euse-demo.c
@@ -0,0 +1,239 @@
+/* osmo-demo-euse: An External USSD Entity (EUSE) for demo purpose */
+
+/* (C) 2018 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/>.
+ */
+
+/*
+ * This program illustrates how to implement an external USSD application using
+ * the existing osmocom libraries, particularly libosmocore, libosmogsm and libosmo-gsup-client.
+ *
+ * It will receive any MS-originated USSD message that is routed to it via the HLR, and
+ * simply respond it quoted in the following string: 'You sent "foobar"' (assuming the original
+ * message was 'foobar').
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/application.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsm/gsm0480.h>
+#include <osmocom/gsm/protocol/gsm_04_80.h>
+
+#include <osmocom/gsupclient/gsup_client.h>
+
+#include "logging.h"
+
+static struct osmo_gsup_client *g_gc;
+
+/*! send a SS/USSD response to a given imsi/session.
+ *  \param[in] gsupc GSUP client connection through which to send
+ *  \param[in] imsi IMSI of the subscriber
+ *  \param[in] session_id Unique identifier of SS session for which this response is
+ *  \param[in] gsup_msg_type GSUP message type (OSMO_GSUP_MSGT_PROC_SS_{REQUEST,RESULT,ERROR})
+ *  \param[in] final Is this the final result (true=END) or an intermediate result (false=CONTINUE)
+ *  \param[in] msg Optional binary/BER encoded SS date (for FACILITY IE). Can be NULL. Freed in
+ *  		   this function call.
+ */
+static int euse_tx_ss(struct osmo_gsup_client *gsupc, const char *imsi, uint32_t session_id,
+		      enum osmo_gsup_message_type gsup_msg_type, bool final, struct msgb *ss_msg)
+{
+	struct osmo_gsup_message resp = {0};
+	struct msgb *resp_msg;
+
+	switch (gsup_msg_type) {
+	case OSMO_GSUP_MSGT_PROC_SS_REQUEST:
+	case OSMO_GSUP_MSGT_PROC_SS_RESULT:
+	case OSMO_GSUP_MSGT_PROC_SS_ERROR:
+		break;
+	default:
+		msgb_free(ss_msg);
+		return -EINVAL;
+	}
+
+	resp.message_type = gsup_msg_type;
+	OSMO_STRLCPY_ARRAY(resp.imsi, imsi);
+	if (final)
+		resp.session_state = OSMO_GSUP_SESSION_STATE_END;
+	else
+		resp.session_state = OSMO_GSUP_SESSION_STATE_CONTINUE;
+	resp.session_id = session_id;
+	if (ss_msg) {
+		resp.ss_info = msgb_data(ss_msg);
+		resp.ss_info_len = msgb_length(ss_msg);
+	}
+
+	resp_msg = gsm0480_msgb_alloc_name(__func__);
+	OSMO_ASSERT(resp_msg);
+	osmo_gsup_encode(resp_msg, &resp);
+	msgb_free(ss_msg);
+	return osmo_gsup_client_send(gsupc, resp_msg);
+}
+
+/*! send a SS/USSD reject to a given IMSI/session.
+ * \param[in] gsupc		GSUP client connection through which to send
+ * \param[in] imsi		IMSI of the subscriber
+ * \param[in] session_id	Unique identifier of SS session for which this response is
+ * \param[in] invoke_id		InvokeID of the request
+ * \param[in] problem_tag	Problem code tag (table 3.13)
+ * \param[in] problem_code	Problem code (table 3.14-3.17)
+ */
+static int euse_tx_ussd_reject(struct osmo_gsup_client *gsupc, const char *imsi, uint32_t session_id,
+				int invoke_id, uint8_t problem_tag, uint8_t problem_code)
+{
+	struct msgb *msg = gsm0480_gen_reject(invoke_id, problem_tag, problem_code);
+	LOGP(DMAIN, LOGL_NOTICE, "Tx %s/0x%08x: Reject(%d, 0x%02x, 0x%02x)\n", imsi, session_id,
+		invoke_id, problem_tag, problem_code);
+	OSMO_ASSERT(msg);
+	return euse_tx_ss(gsupc, imsi, session_id, OSMO_GSUP_MSGT_PROC_SS_RESULT, true, msg);
+}
+
+/*! send a SS/USSD response in 7-bit GSM default alphabet o a given imsi/session.
+ * \param[in] gsupc		GSUP client connection through which to send
+ * \param[in] imsi		IMSI of the subscriber
+ * \param[in] session_id	Unique identifier of SS session for which this response is
+ * \param[in] final		Is this the final result (true=END) or an intermediate result
+ * 				(false=CONTINUE)
+ * \param[in] invoke_id		InvokeID of the request
+ */
+static int euse_tx_ussd_resp_7bit(struct osmo_gsup_client *gsupc, const char *imsi, uint32_t session_id,
+				  bool final, uint8_t invoke_id, const char *text)
+{
+	struct msgb *ss_msg;
+
+	/* encode response; remove L3 header */
+	ss_msg = gsm0480_gen_ussd_resp_7bit(invoke_id, text);
+	LOGP(DMAIN, LOGL_DEBUG, "Tx %s/0x%08x: USSD Result(%d, %s, '%s')\n", imsi, session_id,
+		invoke_id, final ? "END" : "CONTINUE", text);
+	OSMO_ASSERT(ss_msg);
+	return euse_tx_ss(gsupc, imsi, session_id, OSMO_GSUP_MSGT_PROC_SS_RESULT, final, ss_msg);
+}
+
+static int euse_rx_proc_ss_req(struct osmo_gsup_client *gsupc, const struct osmo_gsup_message *gsup)
+{
+	char buf[GSM0480_USSD_7BIT_STRING_LEN+1];
+	struct ss_request req = {0};
+
+	if (gsup->ss_info && gsup->ss_info_len) {
+		if (gsm0480_parse_facility_ie(gsup->ss_info, gsup->ss_info_len, &req)) {
+			return euse_tx_ussd_reject(gsupc, gsup->imsi, gsup->session_id, -1,
+						   GSM_0480_PROBLEM_CODE_TAG_GENERAL,
+						   GSM_0480_GEN_PROB_CODE_BAD_STRUCTURE);
+		}
+	}
+
+	LOGP(DMAIN, LOGL_INFO, "Rx %s/0x%08x: USSD SessionState=%s, OpCode=%s, '%s'\n", gsup->imsi,
+		gsup->session_id, osmo_gsup_session_state_name(gsup->session_state),
+		gsm0480_op_code_name(req.opcode), req.ussd_text);
+
+	/* we only handle single-request-response USSD in this demo */
+	if (gsup->session_state != OSMO_GSUP_SESSION_STATE_BEGIN) {
+		return euse_tx_ussd_reject(gsupc, gsup->imsi, gsup->session_id, req.invoke_id,
+					   GSM_0480_PROBLEM_CODE_TAG_GENERAL,
+					   GSM_0480_GEN_PROB_CODE_UNRECOGNISED);
+	}
+
+	snprintf(buf, sizeof(buf), "You sent \"%s\"", req.ussd_text);
+	return euse_tx_ussd_resp_7bit(gsupc, gsup->imsi, gsup->session_id, true, req.invoke_id, buf);
+}
+
+static int gsupc_read_cb(struct osmo_gsup_client *gsupc, struct msgb *msg)
+{
+	struct osmo_gsup_message gsup_msg = {0};
+	int rc;
+
+	rc = osmo_gsup_decode(msgb_l2(msg), msgb_l2len(msg), &gsup_msg);
+	if (rc < 0) {
+		LOGP(DMAIN, LOGL_ERROR, "Error decoding GSUP: %s\n", msgb_hexdump(msg));
+		return rc;
+	}
+	DEBUGP(DMAIN, "Rx GSUP %s: %s\n", osmo_gsup_message_type_name(gsup_msg.message_type),
+		msgb_hexdump(msg));
+
+	switch (gsup_msg.message_type) {
+	case OSMO_GSUP_MSGT_PROC_SS_REQUEST:
+	case OSMO_GSUP_MSGT_PROC_SS_RESULT:
+		euse_rx_proc_ss_req(gsupc, &gsup_msg);
+		break;
+	case OSMO_GSUP_MSGT_PROC_SS_ERROR:
+		break;
+	default:
+		LOGP(DMAIN, LOGL_DEBUG, "Unhandled GSUP message type %s\n",
+			osmo_gsup_message_type_name(gsup_msg.message_type));
+		break;
+	}
+
+	msgb_free(msg);
+	return 0;
+}
+
+
+static struct log_info_cat default_categories[] = {
+	[DMAIN] = {
+		.name = "DMAIN",
+		.description = "Main Program",
+		.enabled = 1, .loglevel = LOGL_DEBUG,
+	},
+};
+
+static const struct log_info gsup_log_info = {
+	.cat = default_categories,
+	.num_cat = ARRAY_SIZE(default_categories),
+};
+
+static void print_usage(void)
+{
+	printf("Usage: osmo-euse-demo [hlr-ip [hlr-gsup-port]]\n");
+}
+
+int main(int argc, char **argv)
+{
+	char *server_host = "127.0.0.1";
+	uint16_t server_port = OSMO_GSUP_PORT;
+	void *ctx = talloc_named_const(NULL, 0, "demo-euse");
+
+	osmo_init_logging2(ctx, &gsup_log_info);
+
+	printf("argc=%d\n", argc);
+
+	if (argc > 1) {
+		if (!strcmp(argv[1], "--help") || !strcmp(argv[1], "-h")) {
+			print_usage();
+			exit(0);
+		} else
+			server_host = argv[1];
+	}
+	if (argc > 2)
+		server_port = atoi(argv[2]);
+
+	g_gc = osmo_gsup_client_create(ctx, "EUSE-foobar", server_host, server_port, gsupc_read_cb, NULL);
+
+	while (1) {
+		osmo_select_main(0);
+	}
+
+	exit(0);
+}
+
diff --git a/src/populate_hlr_db.pl b/src/populate_hlr_db.pl
new file mode 100755
index 0000000..7be93d8
--- /dev/null
+++ b/src/populate_hlr_db.pl
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+#
+use strict;
+use DBI;
+my $dbh = DBI->connect("dbi:SQLite:dbname=hlr.db","","");
+
+my $sth_subscr_base = $dbh->prepare("INSERT INTO subscriber (imsi, msisdn) VALUES (?, ?)");
+my $sth_subscr_get_id = $dbh->prepare("SELECT * FROM subscriber WHERE imsi = ?");
+my $sth_auc_3g = $dbh->prepare("INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, op, sqn) VALUES (?, ?, ?, ?, ?)");
+my $sth_auc_2g = $dbh->prepare("INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki) VALUES (?, ?, ?)");
+
+sub create_subscr_base($)
+{
+	my ($imsi) = @_;
+	my $suffix = substr($imsi, 5);
+
+	my $msisdn = "49" . $suffix;
+
+	return $sth_subscr_base->execute($imsi, $msisdn);
+}
+
+sub create_auc_2g($)
+{
+	my ($id) = @_;
+
+	my $ki = "000102030405060708090a0b0c0d0e0f";
+
+	$sth_auc_2g->execute($id, 1, $ki);
+}
+
+sub create_auc_3g($)
+{
+	my ($id) = @_;
+
+	my $k = "000102030405060708090a0b0c0d0e0f";
+	my $op = "00102030405060708090a0b0c0d0e0f0";
+
+	$sth_auc_3g->execute($id, 5, $k, $op, 0);
+}
+
+sub create_subscr($$$)
+{
+	my ($imsi, $is_2g, $is_3g) = @_;
+	my $suffix = substr($imsi, 5);
+
+	create_subscr_base($imsi);
+
+	my $id = $dbh->sqlite_last_insert_rowid();
+	#$sth_subscr_get_id->execute($imsi);
+	#my @arr = $sth_subscr_get_id->fetchrow_array();
+	#my $id = $arr[0];
+
+	if ($is_3g) {
+		create_auc_3g($id);
+	}
+	if ($is_2g) {
+		create_auc_2g($id);
+	}
+}
+
+
+my $prefix = "90179";
+
+$dbh->{AutoCommit} = 0;
+$dbh->do("PRAGMA synchronous = OFF");
+
+for (my $i = 0; $i < 1000000; $i++) {
+	my $imsi = sprintf("%s%010u", $prefix, $i);
+	if ($i % 1000 == 0) {
+		printf("Creating subscriber IMSI %s\n", $imsi);
+	}
+	create_subscr($imsi, 1, 1);
+}
+
+$dbh->commit;
diff --git a/src/rand.h b/src/rand.h
new file mode 100644
index 0000000..9c5aedf
--- /dev/null
+++ b/src/rand.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include <stdint.h>
+
+int rand_init(void);
+
+int rand_get(uint8_t *rand, unsigned int len);
diff --git a/src/rand_fake.c b/src/rand_fake.c
new file mode 100644
index 0000000..ad0cc68
--- /dev/null
+++ b/src/rand_fake.c
@@ -0,0 +1,52 @@
+/* (C) 2012 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 <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+static uint8_t ctr = 0;
+
+static void print_msg(void)
+{
+	static int printed = 0;
+	if (!printed) {
+		fprintf(stderr, "Using fake random generator for deterministic "
+			"test results. NEVER USE THIS IN PRODUCTION\n");
+		printed = 1;
+	}
+}
+
+int rand_init(void)
+{
+	print_msg();
+	return 0;
+}
+
+int rand_get(uint8_t *rand, unsigned int len)
+{
+	print_msg();
+	memset(rand, ctr, len);
+	ctr++;
+	return len;
+}
diff --git a/src/rand_urandom.c b/src/rand_urandom.c
new file mode 100644
index 0000000..68243ca
--- /dev/null
+++ b/src/rand_urandom.c
@@ -0,0 +1,38 @@
+/* (C) 2012 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 <stdio.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+static int rand_fd = -1;
+int rand_init(void)
+{
+	rand_fd = open("/dev/urandom", O_RDONLY);
+
+	return rand_fd;
+}
+
+int rand_get(uint8_t *rand, unsigned int len)
+{
+	return read(rand_fd, rand, len);
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..e07749c
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,98 @@
+SUBDIRS = \
+	auc \
+	gsup_server \
+	db \
+	gsup \
+	$(NULL)
+
+# The `:;' works around a Bash 3.2 bug when the output is not writeable.
+$(srcdir)/package.m4: $(top_srcdir)/configure.ac
+	:;{ \
+               echo '# Signature of the current package.' && \
+               echo 'm4_define([AT_PACKAGE_NAME],' && \
+               echo '  [$(PACKAGE_NAME)])' && \
+               echo 'm4_define([AT_PACKAGE_TARNAME],' && \
+               echo '  [$(PACKAGE_TARNAME)])' && \
+               echo 'm4_define([AT_PACKAGE_VERSION],' && \
+               echo '  [$(PACKAGE_VERSION)])' && \
+               echo 'm4_define([AT_PACKAGE_STRING],' && \
+               echo '  [$(PACKAGE_STRING)])' && \
+               echo 'm4_define([AT_PACKAGE_BUGREPORT],' && \
+               echo '  [$(PACKAGE_BUGREPORT)])'; \
+               echo 'm4_define([AT_PACKAGE_URL],' && \
+               echo '  [$(PACKAGE_URL)])'; \
+             } >'$(srcdir)/package.m4'
+
+EXTRA_DIST = \
+	testsuite.at \
+	$(srcdir)/package.m4 \
+	$(TESTSUITE) \
+	test_nodes.vty \
+	test_subscriber.vty \
+	test_subscriber.sql \
+	test_subscriber.ctrl \
+	$(NULL)
+
+TESTSUITE = $(srcdir)/testsuite
+
+DISTCLEANFILES = \
+	atconfig \
+	$(NULL)
+
+if ENABLE_EXT_TESTS
+python-tests:
+# don't run vty and ctrl tests concurrently so that the ports don't conflict
+	$(MAKE) vty-test
+	$(MAKE) ctrl-test
+else
+python-tests:
+	echo "Not running python-based external tests (determined at configure-time)"
+endif
+
+VTY_TEST_DB = hlr_vty_test.db
+
+# To update the VTY script from current application behavior,
+# pass -u to vty_script_runner.py by doing:
+#   make vty-test U=-u
+vty-test:
+	-rm -f $(VTY_TEST_DB)
+	osmo_verify_transcript_vty.py -v \
+		-n OsmoHLR -p 4258 \
+		-r "$(top_builddir)/src/osmo-hlr -c $(top_srcdir)/doc/examples/osmo-hlr.cfg -l $(VTY_TEST_DB)" \
+		$(U) $(srcdir)/*.vty
+	-rm -f $(VTY_TEST_DB)
+	-rm $(VTY_TEST_DB)-*
+
+CTRL_TEST_DB = hlr_ctrl_test.db
+
+# To update the CTRL script from current application behavior,
+# pass -u to ctrl_script_runner.py by doing:
+#   make ctrl-test U=-u
+ctrl-test:
+	-rm -f $(CTRL_TEST_DB)
+	sqlite3 $(CTRL_TEST_DB) < $(top_srcdir)/sql/hlr.sql
+	sqlite3 $(CTRL_TEST_DB) < $(srcdir)/test_subscriber.sql
+	osmo_verify_transcript_ctrl.py -v \
+		-p 4259 \
+		-r "$(top_builddir)/src/osmo-hlr -c $(top_srcdir)/doc/examples/osmo-hlr.cfg -l $(CTRL_TEST_DB)" \
+		$(U) $(srcdir)/*.ctrl
+	-rm -f $(CTRL_TEST_DB)
+	-rm $(CTRL_TEST_DB)-*
+
+check-local: atconfig $(TESTSUITE)
+	$(SHELL) '$(TESTSUITE)' $(TESTSUITEFLAGS)
+	$(MAKE) $(AM_MAKEFLAGS) python-tests
+
+installcheck-local: atconfig $(TESTSUITE)
+	$(SHELL) '$(TESTSUITE)' AUTOTEST_PATH='$(bindir)' \
+		$(TESTSUITEFLAGS)
+
+clean-local:
+	test ! -f '$(TESTSUITE)' || \
+		$(SHELL) '$(TESTSUITE)' --clean
+
+AUTOM4TE = $(SHELL) $(top_srcdir)/missing --run autom4te
+AUTOTEST = $(AUTOM4TE) --language=autotest
+$(TESTSUITE): $(srcdir)/testsuite.at $(srcdir)/package.m4
+	$(AUTOTEST) -I '$(srcdir)' -o $@.tmp $@.at
+	mv $@.tmp $@
diff --git a/tests/auc/Makefile.am b/tests/auc/Makefile.am
new file mode 100644
index 0000000..9f2974f
--- /dev/null
+++ b/tests/auc/Makefile.am
@@ -0,0 +1,57 @@
+SUBDIRS = gen_ts_55_205_test_sets
+
+AM_CPPFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir)/src \
+	$(NULL)
+
+AM_CFLAGS = \
+	-Wall \
+	-ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = \
+	$(NULL)
+
+EXTRA_DIST = \
+	auc_test.ok \
+	auc_test.err \
+	auc_ts_55_205_test_sets.ok \
+	auc_ts_55_205_test_sets.err \
+	$(NULL)
+
+check_PROGRAMS = auc_ts_55_205_test_sets
+
+noinst_PROGRAMS = auc_test
+
+auc_test_SOURCES = \
+	auc_test.c \
+	$(NULL)
+
+auc_test_LDADD = \
+	$(top_srcdir)/src/auc.c \
+	$(top_srcdir)/src/logging.c \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+auc_ts_55_205_test_sets_SOURCES = \
+	$(builddir)/auc_ts_55_205_test_sets.c \
+	$(NULL)
+
+auc_ts_55_205_test_sets_LDADD = \
+	$(top_srcdir)/src/auc.c \
+	$(top_srcdir)/src/logging.c \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+auc_ts_55_205_test_sets.c: $(top_srcdir)/tests/auc/gen_ts_55_205_test_sets/*
+	$(top_srcdir)/tests/auc/gen_ts_55_205_test_sets/pdftxt_2_c.py > $@
+
+.PHONY: update_exp
+update_exp:
+	$(builddir)/auc_test >"$(srcdir)/auc_test.ok" 2>"$(srcdir)/auc_test.err"
+	$(builddir)/auc_ts_55_205_test_sets >"$(srcdir)/auc_ts_55_205_test_sets.ok" 2>"$(srcdir)/auc_ts_55_205_test_sets.err"
diff --git a/tests/auc/auc_test.c b/tests/auc/auc_test.c
new file mode 100644
index 0000000..e23cc02
--- /dev/null
+++ b/tests/auc/auc_test.c
@@ -0,0 +1,629 @@
+/* (C) 2016 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 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 <string.h>
+#include <inttypes.h>
+#include <getopt.h>
+
+#include <osmocom/core/application.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+
+#include <osmocom/crypt/auth.h>
+
+#include "logging.h"
+#include "auc.h"
+
+#define comment_start() fprintf(stderr, "\n===== %s\n", __func__);
+#define comment_end() fprintf(stderr, "===== %s: SUCCESS\n\n", __func__);
+
+#define VERBOSE_ASSERT(val, expect_op, fmt) \
+	do { \
+		fprintf(stderr, #val " == " fmt "\n", (val)); \
+		OSMO_ASSERT((val) expect_op); \
+	} while (0);
+
+char *vec_str(const struct osmo_auth_vector *vec)
+{
+	static char buf[1024];
+	char *pos = buf;
+	char *end = buf + sizeof(buf);
+
+#define append(what) \
+	if (pos >= end) \
+		return buf; \
+	pos += snprintf(pos, sizeof(buf) - (pos - buf), \
+                        "  " #what ": %s\n", \
+			osmo_hexdump_nospc((void*)&vec->what, sizeof(vec->what)))
+
+	append(rand);
+	append(autn);
+	append(ck);
+	append(ik);
+	append(res);
+	append(res_len);
+	append(kc);
+	append(sres);
+	append(auth_types);
+#undef append
+
+	return buf;
+}
+
+#define VEC_IS(vec, expect) do { \
+		char *_is = vec_str(vec); \
+	        if (strcmp(_is, expect)) { \
+			fprintf(stderr, "MISMATCH! expected ==\n%s\n", \
+				expect); \
+			char *a = _is; \
+			char *b = expect; \
+			for (; *a && *b; a++, b++) { \
+				if (*a != *b) { \
+					fprintf(stderr, "mismatch at %d:\n", \
+						(int)(a - _is)); \
+					while (a > _is && *(a-1) != '\n') { \
+						fprintf(stderr, " "); \
+						a--; \
+					} \
+					fprintf(stderr, "v\n%s", a); \
+					break; \
+				} \
+			} \
+			OSMO_ASSERT(false); \
+		} else \
+			fprintf(stderr, "vector matches expectations\n"); \
+	} while (0)
+
+uint8_t fake_rand[16] = { 0 };
+bool fake_rand_fixed = true;
+
+void next_rand(const char *hexstr, bool fixed)
+{
+	osmo_hexparse(hexstr, fake_rand, sizeof(fake_rand));
+	fake_rand_fixed = fixed;
+}
+
+int rand_get(uint8_t *rand, unsigned int len)
+{
+	int i;
+	OSMO_ASSERT(len <= sizeof(fake_rand));
+	memcpy(rand, fake_rand, len);
+	if (!fake_rand_fixed) {
+		for (i = 0; i < len; i++)
+			fake_rand[i] += 0x11;
+	}
+	return len;
+}
+
+static void test_gen_vectors_2g_only(void)
+{
+	struct osmo_sub_auth_data aud2g;
+	struct osmo_sub_auth_data aud3g;
+	struct osmo_auth_vector vec;
+	int rc;
+
+	comment_start();
+
+	aud2g = (struct osmo_sub_auth_data){
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = OSMO_AUTH_ALG_COMP128v1,
+	};
+
+	osmo_hexparse("EB215756028D60E3275E613320AEC880",
+		      aud2g.u.gsm.ki, sizeof(aud2g.u.gsm.ki));
+
+	aud3g = (struct osmo_sub_auth_data){ 0 };
+
+	next_rand("39fa2f4e3d523d8619a73b4f65c3e14d", true);
+
+	vec = (struct osmo_auth_vector){ {0} };
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 0, "%"PRIu64);
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+
+	VEC_IS(&vec,
+	       "  rand: 39fa2f4e3d523d8619a73b4f65c3e14d\n"
+	       "  autn: 00000000000000000000000000000000\n"
+	       "  ck: 00000000000000000000000000000000\n"
+	       "  ik: 00000000000000000000000000000000\n"
+	       "  res: 00000000000000000000000000000000\n"
+	       "  res_len: 00\n"
+	       "  kc: 241a5b16aeb8e400\n"
+	       "  sres: 429d5b27\n"
+	       "  auth_types: 01000000\n"
+	      );
+
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 0, "%"PRIu64);
+
+	/* even though vec is not zero-initialized, it should produce the same
+	 * result (regardless of the umts sequence nr) */
+	aud3g.u.umts.sqn = 123;
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+
+	VEC_IS(&vec,
+	       "  rand: 39fa2f4e3d523d8619a73b4f65c3e14d\n"
+	       "  autn: 00000000000000000000000000000000\n"
+	       "  ck: 00000000000000000000000000000000\n"
+	       "  ik: 00000000000000000000000000000000\n"
+	       "  res: 00000000000000000000000000000000\n"
+	       "  res_len: 00\n"
+	       "  kc: 241a5b16aeb8e400\n"
+	       "  sres: 429d5b27\n"
+	       "  auth_types: 01000000\n"
+	      );
+
+	comment_end();
+}
+
+static void test_gen_vectors_2g_plus_3g(void)
+{
+	struct osmo_sub_auth_data aud2g;
+	struct osmo_sub_auth_data aud3g;
+	struct osmo_auth_vector vec;
+	int rc;
+
+	comment_start();
+
+	aud2g = (struct osmo_sub_auth_data){
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = OSMO_AUTH_ALG_COMP128v1,
+	};
+
+	osmo_hexparse("EB215756028D60E3275E613320AEC880",
+		      aud2g.u.gsm.ki, sizeof(aud2g.u.gsm.ki));
+
+	aud3g = (struct osmo_sub_auth_data){
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = OSMO_AUTH_ALG_MILENAGE,
+		.u.umts.sqn = 31,
+	};
+
+	osmo_hexparse("EB215756028D60E3275E613320AEC880",
+		      aud3g.u.umts.k, sizeof(aud3g.u.umts.k));
+	osmo_hexparse("FB2A3D1B360F599ABAB99DB8669F8308",
+		      aud3g.u.umts.opc, sizeof(aud3g.u.umts.opc));
+	next_rand("39fa2f4e3d523d8619a73b4f65c3e14d", true);
+
+	vec = (struct osmo_auth_vector){ {0} };
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 31, "%"PRIu64);
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 32, "%"PRIu64);
+
+	VEC_IS(&vec,
+	       "  rand: 39fa2f4e3d523d8619a73b4f65c3e14d\n"
+	       "  autn: 8704f5ba55d30000541dde77ea5b1d8c\n"
+	       "  ck: f64735036e5871319c679f4742a75ea1\n"
+	       "  ik: 27497388b6cb044648f396aa155b95ef\n"
+	       "  res: e229c19e791f2e410000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: 241a5b16aeb8e400\n"
+	       "  sres: 429d5b27\n"
+	       "  auth_types: 03000000\n"
+	      );
+
+	/* even though vec is not zero-initialized, it should produce the same
+	 * result with the same sequence nr */
+	aud3g.u.umts.sqn = 31;
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 31, "%"PRIu64);
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 32, "%"PRIu64);
+
+	VEC_IS(&vec,
+	       "  rand: 39fa2f4e3d523d8619a73b4f65c3e14d\n"
+	       "  autn: 8704f5ba55d30000541dde77ea5b1d8c\n"
+	       "  ck: f64735036e5871319c679f4742a75ea1\n"
+	       "  ik: 27497388b6cb044648f396aa155b95ef\n"
+	       "  res: e229c19e791f2e410000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: 241a5b16aeb8e400\n"
+	       "  sres: 429d5b27\n"
+	       "  auth_types: 03000000\n"
+	      );
+
+	comment_end();
+}
+
+void _test_gen_vectors_3g_only__expect_vecs(struct osmo_auth_vector vecs[3])
+{
+	fprintf(stderr, "[0]: ");
+	VEC_IS(&vecs[0],
+	       "  rand: 897210a0f7de278f0b8213098e098a3f\n"
+	       "  autn: c6b9790dad4b00000cf322869ea6a481\n"
+	       "  ck: e9922bd036718ed9e40bd1d02c3b81a5\n"
+	       "  ik: f19c20ca863137f8892326d959ec5e01\n"
+	       "  res: 9af5a557902d2db80000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: 7526fc13c5976685\n"
+	       "  sres: 0ad888ef\n"
+	       "  auth_types: 03000000\n"
+	      );
+	fprintf(stderr, "[1]: ");
+	VEC_IS(&vecs[1],
+	       "  rand: 9a8321b108ef38a01c93241a9f1a9b50\n"
+	       "  autn: 79a5113eb0910000be6020540503ffc5\n"
+	       "  ck: 3686f05df057d1899c66ae4eb18cf941\n"
+	       "  ik: 79f21ed53bcb47787de57d136ff803a5\n"
+	       "  res: 43023475cb29292c0000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: aef73dd515e86c15\n"
+	       "  sres: 882b1d59\n"
+	       "  auth_types: 03000000\n"
+	      );
+	fprintf(stderr, "[2]: ");
+	VEC_IS(&vecs[2],
+	       "  rand: ab9432c2190049b12da4352bb02bac61\n"
+	       "  autn: 24b018d46c3b00009c7e1b47f3a19b2b\n"
+	       "  ck: d86c3191a36fc0602e48202ef2080964\n"
+	       "  ik: 648dab72016181406243420649e63dc9\n"
+	       "  res: 010cab11cc63a6e40000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: f0eaf8cb19e0758d\n"
+	       "  sres: cd6f0df5\n"
+	       "  auth_types: 03000000\n"
+	      );
+}
+
+static void test_gen_vectors_3g_only(void)
+{
+	struct osmo_sub_auth_data aud2g;
+	struct osmo_sub_auth_data aud3g;
+	struct osmo_auth_vector vec;
+	struct osmo_auth_vector vecs[3];
+	uint8_t auts[14];
+	uint8_t rand_auts[16];
+	int rc;
+
+	comment_start();
+
+	aud2g = (struct osmo_sub_auth_data){ 0 };
+
+	aud3g = (struct osmo_sub_auth_data){
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = OSMO_AUTH_ALG_MILENAGE,
+		.u.umts.sqn = 31,
+	};
+
+	osmo_hexparse("EB215756028D60E3275E613320AEC880",
+		      aud3g.u.umts.k, sizeof(aud3g.u.umts.k));
+	osmo_hexparse("FB2A3D1B360F599ABAB99DB8669F8308",
+		      aud3g.u.umts.opc, sizeof(aud3g.u.umts.opc));
+	next_rand("39fa2f4e3d523d8619a73b4f65c3e14d", true);
+
+	vec = (struct osmo_auth_vector){ {0} };
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 31, "%"PRIu64);
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 32, "%"PRIu64);
+
+	VEC_IS(&vec,
+	       "  rand: 39fa2f4e3d523d8619a73b4f65c3e14d\n"
+	       "  autn: 8704f5ba55d30000541dde77ea5b1d8c\n"
+	       "  ck: f64735036e5871319c679f4742a75ea1\n"
+	       "  ik: 27497388b6cb044648f396aa155b95ef\n"
+	       "  res: e229c19e791f2e410000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: 059a4f668f6fbe39\n"
+	       "  sres: 9b36efdf\n"
+	       "  auth_types: 03000000\n"
+	      );
+
+	/* Note: 3GPP TS 33.102 6.8.1.2: c3 function to get GSM auth is
+	 * KC[0..7] == CK[0..7] ^ CK[8..15] ^ IK[0..7] ^ IK[8..15]
+	 * In [16]: hex(  0xf64735036e587131
+	 *              ^ 0x9c679f4742a75ea1
+	 *              ^ 0x27497388b6cb0446
+	 *              ^ 0x48f396aa155b95ef)
+	 * Out[16]: '0x59a4f668f6fbe39L'
+	 * hence expecting kc: 059a4f668f6fbe39
+	 */
+
+	/* even though vec is not zero-initialized, it should produce the same
+	 * result with the same sequence nr */
+	aud3g.u.umts.sqn = 31;
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 31, "%"PRIu64);
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 32, "%"PRIu64);
+
+	VEC_IS(&vec,
+	       "  rand: 39fa2f4e3d523d8619a73b4f65c3e14d\n"
+	       "  autn: 8704f5ba55d30000541dde77ea5b1d8c\n"
+	       "  ck: f64735036e5871319c679f4742a75ea1\n"
+	       "  ik: 27497388b6cb044648f396aa155b95ef\n"
+	       "  res: e229c19e791f2e410000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: 059a4f668f6fbe39\n"
+	       "  sres: 9b36efdf\n"
+	       "  auth_types: 03000000\n"
+	      );
+
+
+	fprintf(stderr, "- test AUTS resync\n");
+	vec = (struct osmo_auth_vector){};
+	aud3g.u.umts.sqn = 31;
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 31, "%"PRIu64);
+
+	/* The AUTN sent was 8704f5ba55f30000d2ee44b22c8ea919
+	 * with the first 6 bytes being SQN ^ AK.
+	 * K = EB215756028D60E3275E613320AEC880
+	 * OPC = FB2A3D1B360F599ABAB99DB8669F8308
+	 * RAND = 39fa2f4e3d523d8619a73b4f65c3e14d
+	 * --milenage-f5-->
+	 * AK = 8704f5ba55f3
+	 *
+	 * The first six bytes are 8704f5ba55f3,
+	 * and 8704f5ba55f3 ^ AK = 0.
+	 * --> SQN = 0.
+	 *
+	 * Say the USIM doesn't like that, let's say it is at SQN 23.
+	 * SQN_MS = 000000000017
+	 *
+	 * AUTS = Conc(SQN_MS) || MAC-S
+	 * Conc(SQN_MS) = SQN_MS ⊕ f5*[K](RAND)
+	 * MAC-S = f1*[K] (SQN MS || RAND || AMF)
+	 *
+	 * f5*--> Conc(SQN_MS) = 000000000017 ^ 979498b1f73a
+	 *                     = 979498b1f72d
+	 * AMF = 0000 (TS 33.102 v7.0.0, 6.3.3)
+	 *
+	 * MAC-S = f1*[K] (000000000017 || 39fa2f4e3d523d8619a73b4f65c3e14d || 0000)
+	 *       = 3e28c59fa2e72f9c
+	 *
+	 * AUTS = 979498b1f72d || 3e28c59fa2e72f9c
+	 *
+	 * verify valid AUTS resulting in SQN 23 with:
+	 * osmo-auc-gen -3 -a milenage -k EB215756028D60E3275E613320AEC880 \
+	 *              -o FB2A3D1B360F599ABAB99DB8669F8308 \
+	 *              -r 39fa2f4e3d523d8619a73b4f65c3e14d \
+	 *              -A 979498b1f72d3e28c59fa2e72f9c
+	 */
+
+	/* AUTS response by USIM */
+	osmo_hexparse("979498b1f72d3e28c59fa2e72f9c",
+		      auts, sizeof(auts));
+	/* RAND sent to USIM, which AUTS was generated from */
+	osmo_hexparse("39fa2f4e3d523d8619a73b4f65c3e14d",
+		      rand_auts, sizeof(rand_auts));
+	/* new RAND token for the next key */
+	next_rand("897210a0f7de278f0b8213098e098a3f", true);
+	rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, rand_auts, auts);
+	VERBOSE_ASSERT(rc, == 1, "%d");
+	/* The USIM's last sqn was 23, the calculated vector was 24 */
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 24, "%"PRIu64);
+
+	VEC_IS(&vec,
+	       "  rand: 897210a0f7de278f0b8213098e098a3f\n"
+	       "  autn: c6b9790dad4b00000cf322869ea6a481\n"
+	       "  ck: e9922bd036718ed9e40bd1d02c3b81a5\n"
+	       "  ik: f19c20ca863137f8892326d959ec5e01\n"
+	       "  res: 9af5a557902d2db80000000000000000\n"
+	       "  res_len: 08\n"
+	       "  kc: 7526fc13c5976685\n"
+	       "  sres: 0ad888ef\n"
+	       "  auth_types: 03000000\n"
+	      );
+
+
+	fprintf(stderr, "- verify N vectors with AUTS resync"
+		" == N vectors without AUTS\n"
+		"First just set rand and sqn = 23, and compute 3 vectors\n");
+	next_rand("897210a0f7de278f0b8213098e098a3f", false);
+	aud3g.u.umts.sqn = 23;
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 23, "%"PRIu64);
+
+	memset(vecs, 0, sizeof(vecs));
+	rc = auc_compute_vectors(vecs, 3, &aud2g, &aud3g, NULL, NULL);
+	VERBOSE_ASSERT(rc, == 3, "%d");
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 26, "%"PRIu64);
+
+	_test_gen_vectors_3g_only__expect_vecs(vecs);
+
+	fprintf(stderr, "Now reach sqn = 23 with AUTS and expect the same\n");
+	/* AUTS response by USIM */
+	osmo_hexparse("979498b1f72d3e28c59fa2e72f9c",
+		      auts, sizeof(auts));
+	/* RAND sent to USIM, which AUTS was generated from */
+	osmo_hexparse("39fa2f4e3d523d8619a73b4f65c3e14d",
+		      rand_auts, sizeof(rand_auts));
+	next_rand("897210a0f7de278f0b8213098e098a3f", false);
+	rc = auc_compute_vectors(vecs, 3, &aud2g, &aud3g, rand_auts, auts);
+
+	_test_gen_vectors_3g_only__expect_vecs(vecs);
+
+	comment_end();
+}
+
+void test_gen_vectors_bad_args()
+{
+	struct osmo_auth_vector vec;
+	uint8_t auts[14];
+	uint8_t rand_auts[16];
+	int rc;
+	int i;
+
+	struct osmo_sub_auth_data aud2g = {
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = OSMO_AUTH_ALG_COMP128v1,
+	};
+
+	struct osmo_sub_auth_data aud3g = {
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = OSMO_AUTH_ALG_MILENAGE,
+	};
+
+	struct osmo_sub_auth_data aud2g_noalg = {
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+
+	struct osmo_sub_auth_data aud3g_noalg = {
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+
+	struct osmo_sub_auth_data aud_notype = {
+		.type = OSMO_AUTH_TYPE_NONE,
+		.algo = OSMO_AUTH_ALG_MILENAGE,
+	};
+
+	struct osmo_sub_auth_data no_aud = {
+		.type = OSMO_AUTH_TYPE_NONE,
+		.algo = OSMO_AUTH_ALG_NONE,
+	};
+
+	struct {
+		struct osmo_sub_auth_data *aud2g;
+		struct osmo_sub_auth_data *aud3g;
+		uint8_t *rand_auts;
+		uint8_t *auts;
+		const char *label;
+	} tests[] = {
+		{         NULL,         NULL,       NULL,  NULL, "no auth data (a)"},
+		{         NULL, &aud3g_noalg,       NULL,  NULL, "no auth data (b)"},
+		{         NULL,  &aud_notype,       NULL,  NULL, "no auth data (c)"},
+		{         NULL,      &no_aud,       NULL,  NULL, "no auth data (d)"},
+		{ &aud2g_noalg,         NULL,       NULL,  NULL, "no auth data (e)"},
+		{ &aud2g_noalg, &aud3g_noalg,       NULL,  NULL, "no auth data (f)"},
+		{ &aud2g_noalg,  &aud_notype,       NULL,  NULL, "no auth data (g)"},
+		{ &aud2g_noalg,      &no_aud,       NULL,  NULL, "no auth data (h)"},
+		{  &aud_notype,         NULL,       NULL,  NULL, "no auth data (i)"},
+		{  &aud_notype, &aud3g_noalg,       NULL,  NULL, "no auth data (j)"},
+		{  &aud_notype,  &aud_notype,       NULL,  NULL, "no auth data (k)"},
+		{  &aud_notype,      &no_aud,       NULL,  NULL, "no auth data (l)"},
+		{      &no_aud,         NULL,       NULL,  NULL, "no auth data (m)"},
+		{      &no_aud, &aud3g_noalg,       NULL,  NULL, "no auth data (n)"},
+		{      &no_aud,  &aud_notype,       NULL,  NULL, "no auth data (o)"},
+		{      &no_aud,      &no_aud,       NULL,  NULL, "no auth data (p)"},
+		{       &aud3g,         NULL,       NULL,  NULL, "wrong auth data type (a)"},
+		{       &aud3g, &aud3g_noalg,       NULL,  NULL, "wrong auth data type (b)"},
+		{       &aud3g,  &aud_notype,       NULL,  NULL, "wrong auth data type (c)"},
+		{       &aud3g,      &no_aud,       NULL,  NULL, "wrong auth data type (d)"},
+		{         NULL,       &aud2g,       NULL,  NULL, "wrong auth data type (e)"},
+		{ &aud3g_noalg,       &aud2g,       NULL,  NULL, "wrong auth data type (f)"},
+		{  &aud_notype,       &aud2g,       NULL,  NULL, "wrong auth data type (g)"},
+		{      &no_aud,       &aud2g,       NULL,  NULL, "wrong auth data type (h)"},
+		{       &aud3g,       &aud2g,       NULL,  NULL, "wrong auth data type (i)"},
+		{       &aud3g,       &aud3g,       NULL,  NULL, "wrong auth data type (j)"},
+		{       &aud2g,       &aud2g,       NULL,  NULL, "wrong auth data type (k)"},
+		{       &aud2g,         NULL,  rand_auts,  auts, "AUTS for 2G-only (a)"},
+		{       &aud2g, &aud3g_noalg,  rand_auts,  auts, "AUTS for 2G-only (b)"},
+		{       &aud2g,  &aud_notype,  rand_auts,  auts, "AUTS for 2G-only (c)"},
+		{       &aud2g,      &no_aud,  rand_auts,  auts, "AUTS for 2G-only (d)"},
+		{         NULL,       &aud3g,       NULL,  auts, "incomplete AUTS (a)"},
+		{         NULL,       &aud3g,  rand_auts,  NULL, "incomplete AUTS (b)"},
+		{       &aud2g,       &aud3g,       NULL,  auts, "incomplete AUTS (c)"},
+		{       &aud2g,       &aud3g,  rand_auts,  NULL, "incomplete AUTS (d)"},
+	};
+
+	comment_start();
+
+	for (i = 0; i < ARRAY_SIZE(tests); i++) {
+		fprintf(stderr, "\n- %s\n", tests[i].label);
+		rc = auc_compute_vectors(&vec, 1,
+					 tests[i].aud2g,
+					 tests[i].aud3g,
+					 tests[i].rand_auts,
+					 tests[i].auts);
+		VERBOSE_ASSERT(rc, < 0, "%d");
+	}
+
+	comment_end();
+}
+
+static struct {
+	bool verbose;
+} cmdline_opts = {
+	.verbose = false,
+};
+
+static void print_help(const char *program)
+{
+	printf("Usage:\n"
+	       "  %s [-v] [N [N...]]\n"
+	       "Options:\n"
+	       "  -h --help      show this text.\n"
+	       "  -v --verbose   print source file and line numbers\n",
+	       program
+	       );
+}
+
+static void handle_options(int argc, char **argv)
+{
+	while (1) {
+		int option_index = 0, c;
+		static struct option long_options[] = {
+			{"help", 0, 0, 'h'},
+			{"verbose", 1, 0, 'v'},
+			{0, 0, 0, 0}
+		};
+
+		c = getopt_long(argc, argv, "hv",
+				long_options, &option_index);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			print_help(argv[0]);
+			exit(0);
+		case 'v':
+			cmdline_opts.verbose = true;
+			break;
+		default:
+			/* catch unknown options *as well as* missing arguments. */
+			fprintf(stderr, "Error in command line options. Exiting.\n");
+			exit(-1);
+			break;
+		}
+	}
+
+	if (optind < argc) {
+		fprintf(stderr, "too many args\n");
+		exit(-1);
+	}
+}
+
+int main(int argc, char **argv)
+{
+	printf("auc_3g_test.c\n");
+
+	handle_options(argc, argv);
+
+	void *tall_ctx = talloc_named_const(NULL, 1, "auc_test");
+
+	osmo_init_logging2(tall_ctx, &hlr_log_info);
+	log_set_print_filename(osmo_stderr_target, cmdline_opts.verbose);
+	log_set_print_timestamp(osmo_stderr_target, 0);
+	log_set_use_color(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+	log_parse_category_mask(osmo_stderr_target, "DMAIN,1:DDB,1:DAUC,1");
+
+	test_gen_vectors_2g_only();
+	test_gen_vectors_2g_plus_3g();
+	test_gen_vectors_3g_only();
+	test_gen_vectors_bad_args();
+
+	printf("Done\n");
+	return 0;
+}
diff --git a/tests/auc/auc_test.err b/tests/auc/auc_test.err
new file mode 100644
index 0000000..0a4d9af
--- /dev/null
+++ b/tests/auc/auc_test.err
@@ -0,0 +1,362 @@
+
+===== test_gen_vectors_2g_only
+aud3g.u.umts.sqn == 0
+DAUC Computing 1 auth vector: 2G only
+DAUC 2G: ki = eb215756028d60e3275e613320aec880
+DAUC vector [0]: rand = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: kc = 241a5b16aeb8e400
+DAUC vector [0]: sres = 429d5b27
+DAUC vector [0]: auth_types = 0x1
+rc == 1
+vector matches expectations
+aud3g.u.umts.sqn == 0
+DAUC Computing 1 auth vector: 2G only
+DAUC 2G: ki = eb215756028d60e3275e613320aec880
+DAUC vector [0]: rand = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: kc = 241a5b16aeb8e400
+DAUC vector [0]: sres = 429d5b27
+DAUC vector [0]: auth_types = 0x1
+rc == 1
+vector matches expectations
+===== test_gen_vectors_2g_only: SUCCESS
+
+
+===== test_gen_vectors_2g_plus_3g
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G + separate 2G
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC 2G: ki = eb215756028d60e3275e613320aec880
+DAUC vector [0]: rand = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 8704f5ba55d30000541dde77ea5b1d8c
+DAUC vector [0]: ck = f64735036e5871319c679f4742a75ea1
+DAUC vector [0]: ik = 27497388b6cb044648f396aa155b95ef
+DAUC vector [0]: res = e229c19e791f2e410000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: calculating 2G separately
+DAUC vector [0]: kc = 241a5b16aeb8e400
+DAUC vector [0]: sres = 429d5b27
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G + separate 2G
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC 2G: ki = eb215756028d60e3275e613320aec880
+DAUC vector [0]: rand = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 8704f5ba55d30000541dde77ea5b1d8c
+DAUC vector [0]: ck = f64735036e5871319c679f4742a75ea1
+DAUC vector [0]: ik = 27497388b6cb044648f396aa155b95ef
+DAUC vector [0]: res = e229c19e791f2e410000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: calculating 2G separately
+DAUC vector [0]: kc = 241a5b16aeb8e400
+DAUC vector [0]: sres = 429d5b27
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_gen_vectors_2g_plus_3g: SUCCESS
+
+
+===== test_gen_vectors_3g_only
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 8704f5ba55d30000541dde77ea5b1d8c
+DAUC vector [0]: ck = f64735036e5871319c679f4742a75ea1
+DAUC vector [0]: ik = 27497388b6cb044648f396aa155b95ef
+DAUC vector [0]: res = e229c19e791f2e410000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 059a4f668f6fbe39
+DAUC vector [0]: sres = 9b36efdf
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 8704f5ba55d30000541dde77ea5b1d8c
+DAUC vector [0]: ck = f64735036e5871319c679f4742a75ea1
+DAUC vector [0]: ik = 27497388b6cb044648f396aa155b95ef
+DAUC vector [0]: res = e229c19e791f2e410000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 059a4f668f6fbe39
+DAUC vector [0]: sres = 9b36efdf
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+- test AUTS resync
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys), with AUTS resync
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 897210a0f7de278f0b8213098e098a3f
+DAUC vector [0]: resync: auts = 979498b1f72d3e28c59fa2e72f9c
+DAUC vector [0]: resync: rand_auts = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: sqn = 24
+DAUC vector [0]: autn = c6b9790dad4b00000cf322869ea6a481
+DAUC vector [0]: ck = e9922bd036718ed9e40bd1d02c3b81a5
+DAUC vector [0]: ik = f19c20ca863137f8892326d959ec5e01
+DAUC vector [0]: res = 9af5a557902d2db80000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 7526fc13c5976685
+DAUC vector [0]: sres = 0ad888ef
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 24
+vector matches expectations
+- verify N vectors with AUTS resync == N vectors without AUTS
+First just set rand and sqn = 23, and compute 3 vectors
+aud3g.u.umts.sqn == 23
+DAUC Computing 3 auth vectors: 3G only (2G derived from 3G keys)
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 23
+DAUC vector [0]: rand = 897210a0f7de278f0b8213098e098a3f
+DAUC vector [0]: sqn = 24
+DAUC vector [0]: autn = c6b9790dad4b00000cf322869ea6a481
+DAUC vector [0]: ck = e9922bd036718ed9e40bd1d02c3b81a5
+DAUC vector [0]: ik = f19c20ca863137f8892326d959ec5e01
+DAUC vector [0]: res = 9af5a557902d2db80000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 7526fc13c5976685
+DAUC vector [0]: sres = 0ad888ef
+DAUC vector [0]: auth_types = 0x3
+DAUC vector [1]: rand = 9a8321b108ef38a01c93241a9f1a9b50
+DAUC vector [1]: sqn = 25
+DAUC vector [1]: autn = 79a5113eb0910000be6020540503ffc5
+DAUC vector [1]: ck = 3686f05df057d1899c66ae4eb18cf941
+DAUC vector [1]: ik = 79f21ed53bcb47787de57d136ff803a5
+DAUC vector [1]: res = 43023475cb29292c0000000000000000
+DAUC vector [1]: res_len = 8
+DAUC vector [1]: deriving 2G from 3G
+DAUC vector [1]: kc = aef73dd515e86c15
+DAUC vector [1]: sres = 882b1d59
+DAUC vector [1]: auth_types = 0x3
+DAUC vector [2]: rand = ab9432c2190049b12da4352bb02bac61
+DAUC vector [2]: sqn = 26
+DAUC vector [2]: autn = 24b018d46c3b00009c7e1b47f3a19b2b
+DAUC vector [2]: ck = d86c3191a36fc0602e48202ef2080964
+DAUC vector [2]: ik = 648dab72016181406243420649e63dc9
+DAUC vector [2]: res = 010cab11cc63a6e40000000000000000
+DAUC vector [2]: res_len = 8
+DAUC vector [2]: deriving 2G from 3G
+DAUC vector [2]: kc = f0eaf8cb19e0758d
+DAUC vector [2]: sres = cd6f0df5
+DAUC vector [2]: auth_types = 0x3
+rc == 3
+aud3g.u.umts.sqn == 26
+[0]: vector matches expectations
+[1]: vector matches expectations
+[2]: vector matches expectations
+Now reach sqn = 23 with AUTS and expect the same
+DAUC Computing 3 auth vectors: 3G only (2G derived from 3G keys), with AUTS resync
+DAUC 3G: k = eb215756028d60e3275e613320aec880
+DAUC 3G: opc = fb2a3d1b360f599abab99db8669f8308
+DAUC 3G: for sqn ind 0, previous sqn was 26
+DAUC vector [0]: rand = 897210a0f7de278f0b8213098e098a3f
+DAUC vector [0]: resync: auts = 979498b1f72d3e28c59fa2e72f9c
+DAUC vector [0]: resync: rand_auts = 39fa2f4e3d523d8619a73b4f65c3e14d
+DAUC vector [0]: sqn = 24
+DAUC vector [0]: autn = c6b9790dad4b00000cf322869ea6a481
+DAUC vector [0]: ck = e9922bd036718ed9e40bd1d02c3b81a5
+DAUC vector [0]: ik = f19c20ca863137f8892326d959ec5e01
+DAUC vector [0]: res = 9af5a557902d2db80000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 7526fc13c5976685
+DAUC vector [0]: sres = 0ad888ef
+DAUC vector [0]: auth_types = 0x3
+DAUC vector [1]: rand = 9a8321b108ef38a01c93241a9f1a9b50
+DAUC vector [1]: sqn = 25
+DAUC vector [1]: autn = 79a5113eb0910000be6020540503ffc5
+DAUC vector [1]: ck = 3686f05df057d1899c66ae4eb18cf941
+DAUC vector [1]: ik = 79f21ed53bcb47787de57d136ff803a5
+DAUC vector [1]: res = 43023475cb29292c0000000000000000
+DAUC vector [1]: res_len = 8
+DAUC vector [1]: deriving 2G from 3G
+DAUC vector [1]: kc = aef73dd515e86c15
+DAUC vector [1]: sres = 882b1d59
+DAUC vector [1]: auth_types = 0x3
+DAUC vector [2]: rand = ab9432c2190049b12da4352bb02bac61
+DAUC vector [2]: sqn = 26
+DAUC vector [2]: autn = 24b018d46c3b00009c7e1b47f3a19b2b
+DAUC vector [2]: ck = d86c3191a36fc0602e48202ef2080964
+DAUC vector [2]: ik = 648dab72016181406243420649e63dc9
+DAUC vector [2]: res = 010cab11cc63a6e40000000000000000
+DAUC vector [2]: res_len = 8
+DAUC vector [2]: deriving 2G from 3G
+DAUC vector [2]: kc = f0eaf8cb19e0758d
+DAUC vector [2]: sres = cd6f0df5
+DAUC vector [2]: auth_types = 0x3
+[0]: vector matches expectations
+[1]: vector matches expectations
+[2]: vector matches expectations
+===== test_gen_vectors_3g_only: SUCCESS
+
+
+===== test_gen_vectors_bad_args
+
+- no auth data (a)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (b)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (c)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (d)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (e)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (f)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (g)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (h)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (i)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (j)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (k)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (l)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (m)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (n)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (o)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- no auth data (p)
+DAUC auc_compute_vectors() called with neither 2G nor 3G auth data available
+rc == -1
+
+- wrong auth data type (a)
+DAUC auc_compute_vectors() called with non-2G auth data passed for aud2g arg
+rc == -1
+
+- wrong auth data type (b)
+DAUC auc_compute_vectors() called with non-2G auth data passed for aud2g arg
+rc == -1
+
+- wrong auth data type (c)
+DAUC auc_compute_vectors() called with non-2G auth data passed for aud2g arg
+rc == -1
+
+- wrong auth data type (d)
+DAUC auc_compute_vectors() called with non-2G auth data passed for aud2g arg
+rc == -1
+
+- wrong auth data type (e)
+DAUC auc_compute_vectors() called with non-3G auth data passed for aud3g arg
+rc == -1
+
+- wrong auth data type (f)
+DAUC auc_compute_vectors() called with non-3G auth data passed for aud3g arg
+rc == -1
+
+- wrong auth data type (g)
+DAUC auc_compute_vectors() called with non-3G auth data passed for aud3g arg
+rc == -1
+
+- wrong auth data type (h)
+DAUC auc_compute_vectors() called with non-3G auth data passed for aud3g arg
+rc == -1
+
+- wrong auth data type (i)
+DAUC auc_compute_vectors() called with non-2G auth data passed for aud2g arg
+rc == -1
+
+- wrong auth data type (j)
+DAUC auc_compute_vectors() called with non-2G auth data passed for aud2g arg
+rc == -1
+
+- wrong auth data type (k)
+DAUC auc_compute_vectors() called with non-3G auth data passed for aud3g arg
+rc == -1
+
+- AUTS for 2G-only (a)
+DAUC auc_compute_vectors() with AUTS called but no 3G auth data passed
+rc == -1
+
+- AUTS for 2G-only (b)
+DAUC auc_compute_vectors() with AUTS called but no 3G auth data passed
+rc == -1
+
+- AUTS for 2G-only (c)
+DAUC auc_compute_vectors() with AUTS called but no 3G auth data passed
+rc == -1
+
+- AUTS for 2G-only (d)
+DAUC auc_compute_vectors() with AUTS called but no 3G auth data passed
+rc == -1
+
+- incomplete AUTS (a)
+DAUC auc_compute_vectors() with only one of AUTS and AUTS_RAND given, need both or neither
+rc == -1
+
+- incomplete AUTS (b)
+DAUC auc_compute_vectors() with only one of AUTS and AUTS_RAND given, need both or neither
+rc == -1
+
+- incomplete AUTS (c)
+DAUC auc_compute_vectors() with only one of AUTS and AUTS_RAND given, need both or neither
+rc == -1
+
+- incomplete AUTS (d)
+DAUC auc_compute_vectors() with only one of AUTS and AUTS_RAND given, need both or neither
+rc == -1
+===== test_gen_vectors_bad_args: SUCCESS
+
diff --git a/tests/auc/auc_test.ok b/tests/auc/auc_test.ok
new file mode 100644
index 0000000..81272cd
--- /dev/null
+++ b/tests/auc/auc_test.ok
@@ -0,0 +1,2 @@
+auc_3g_test.c
+Done
diff --git a/tests/auc/auc_ts_55_205_test_sets.err b/tests/auc/auc_ts_55_205_test_sets.err
new file mode 100644
index 0000000..b0f83ae
--- /dev/null
+++ b/tests/auc/auc_ts_55_205_test_sets.err
@@ -0,0 +1,437 @@
+
+===== test_set_1
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 465b5ce8b199b49faa5f0a2ee238a6bc
+DAUC 3G: opc = cd63cb71954a9f4e48a5994e37a02baf
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 23553cbe9637a89d218ae64dae47bf35
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = aa689c64835000002bb2bf2f1faba139
+DAUC vector [0]: ck = b40ba9a3c58b2a05bbf0d987b21bf8cb
+DAUC vector [0]: ik = f769bcd751044604127672711c6d3441
+DAUC vector [0]: res = a54211d5e3ba50bf0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = eae4be823af9a08b
+DAUC vector [0]: sres = 46f8416a
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_1: SUCCESS
+
+
+===== test_set_2
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = fec86ba6eb707ed08905757b1bb44b8f
+DAUC 3G: opc = 1006020f0a478bf6b699f15c062e42b3
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 9f7c8d021accf4db213ccff0c7f71a6a
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 33484dc2134b000091ec125f4840ed64
+DAUC vector [0]: ck = 5dbdbb2954e8f3cde665b046179a5098
+DAUC vector [0]: ik = 59a92d3b476a0443487055cf88b2307b
+DAUC vector [0]: res = 8011c48c0c214ed20000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = aa01739b8caa976d
+DAUC vector [0]: sres = 8c308a5e
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_2: SUCCESS
+
+
+===== test_set_3
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 9e5944aea94b81165c82fbf9f32db751
+DAUC 3G: opc = a64a507ae1a2a98bb88eb4210135dc87
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = ce83dbc54ac0274a157c17f80d017bd6
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = f0b9c08ad00e00005da4ccbbdfa29310
+DAUC vector [0]: ck = e203edb3971574f5a94b0d61b816345d
+DAUC vector [0]: ik = 0c4524adeac041c4dd830d20854fc46b
+DAUC vector [0]: res = f365cd683cd92e960000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 9a8ec95f408cc507
+DAUC vector [0]: sres = cfbce3fe
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_3: SUCCESS
+
+
+===== test_set_4
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 4ab1deb05ca6ceb051fc98e77d026a84
+DAUC 3G: opc = dcf07cbd51855290b92a07a9891e523e
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 74b0cd6031a1c8339b2b6ce2b8c4a186
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 31e11a60913800006a7003718d5d82e5
+DAUC vector [0]: ck = 7657766b373d1c2138f307e3de9242f9
+DAUC vector [0]: ik = 1c42e960d89b8fa99f2744e0708ccb53
+DAUC vector [0]: res = 5860fc1bce351e7e0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = cdc1dc0841b81a22
+DAUC vector [0]: sres = 9655e265
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_4: SUCCESS
+
+
+===== test_set_5
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 6c38a116ac280c454f59332ee35c8c4f
+DAUC 3G: opc = 3803ef5363b947c6aaa225e58fae3934
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = ee6466bc96202c5a557abbeff8babf63
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 45b0f69ab04c000053f2a822f2b3e824
+DAUC vector [0]: ck = 3f8c7587fe8e4b233af676aede30ba3b
+DAUC vector [0]: ik = a7466cc1e6b2a1337d49d3b66e95d7b4
+DAUC vector [0]: res = 16c8233f05a0ac280000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = df75bc5ea899879f
+DAUC vector [0]: sres = 13688f17
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_5: SUCCESS
+
+
+===== test_set_6
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 2d609d4db0ac5bf0d2c0de267014de0d
+DAUC 3G: opc = c35a0ab0bcbfc9252caff15f24efbde0
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 194aa756013896b74b4a2a3b0af4539e
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 7e6455f34cd300004a2a9f2f3a529b8c
+DAUC vector [0]: ck = 4cd0846020f8fa0731dd47cbdc6be411
+DAUC vector [0]: ik = 88ab80a415f15c73711254a1d388f696
+DAUC vector [0]: res = 8c25a16cd918a1df0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 84b417ae3aeab4f3
+DAUC vector [0]: sres = 553d00b3
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_6: SUCCESS
+
+
+===== test_set_7
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = a530a7fe428fad1082c45eddfce13884
+DAUC 3G: opc = 27953e49bc8af6dcc6e730eb80286be3
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 3a4c2b3245c50eb5c71d08639395764d
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 88196c47984f00000a50c5f4056ccb68
+DAUC vector [0]: ck = 10f05bab75a99a5fbb98a9c287679c3b
+DAUC vector [0]: ik = f9ec0865eb32f22369cade40c59c3a44
+DAUC vector [0]: res = a63241e1ffc3e5ab0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 3b4e244cdc60ce03
+DAUC vector [0]: sres = 59f1a44a
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_7: SUCCESS
+
+
+===== test_set_8
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = d9151cf04896e25830bf2e08267b8360
+DAUC 3G: opc = c4c93effe8a08138c203d4c27ce4e3d9
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = f761e5e93d603feb730e27556cb8a2ca
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 82a0f5287a5100006d6c0ff132426479
+DAUC vector [0]: ck = 71236b7129f9b22ab77ea7a54c96da22
+DAUC vector [0]: ik = 90527ebaa5588968db41727325a04d9e
+DAUC vector [0]: res = 4a90b2171ac83a760000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 8d4ec01de597acfe
+DAUC vector [0]: sres = 50588861
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_8: SUCCESS
+
+
+===== test_set_9
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = a0e2971b6822e8d354a18cc235624ecb
+DAUC 3G: opc = 82a26f22bba9e9488f949a10d98e9cc4
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 08eff828b13fdb562722c65c7f30a9b2
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = a2f858aa9e7d00001c14f5fcd445bc46
+DAUC vector [0]: ck = 08cef6d004ec61471a3c3cda048137fa
+DAUC vector [0]: ik = ed0318ca5deb9206272f6e8fa64ba411
+DAUC vector [0]: res = 4bc2212d8624910a0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = d8debc4ffbcd60aa
+DAUC vector [0]: sres = cde6b027
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_9: SUCCESS
+
+
+===== test_set_10
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 0da6f7ba86d5eac8a19cf563ac58642d
+DAUC 3G: opc = 0db1071f8767562ca43a0a64c41e8d08
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 679ac4dbacd7d233ff9d6806f4149ce3
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 4c539a26e1da000071cc0b769fd1aa96
+DAUC vector [0]: ck = 69b1cae7c7429d975e245cacb05a517c
+DAUC vector [0]: ik = 74f24e8c26df58e1b38d7dcd4f1b7fbd
+DAUC vector [0]: res = 6fc30fee6d1235230000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = f0eaa50a1edcebb7
+DAUC vector [0]: sres = 02d13acd
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_10: SUCCESS
+
+
+===== test_set_11
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 77b45843c88e58c10d202684515ed430
+DAUC 3G: opc = d483afae562409a326b5bb0b20c4d762
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 4c47eb3076dc55fe5106cb2034b8cd78
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 30ff25cdadd60000e08a00f7ed54d6fe
+DAUC vector [0]: ck = 908c43f0569cb8f74bc971e706c36c5f
+DAUC vector [0]: ik = c251df0d888dd9329bcf46655b226e40
+DAUC vector [0]: res = aefa357beac2a87a0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 82dbab7f83f063da
+DAUC vector [0]: sres = 44389d01
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_11: SUCCESS
+
+
+===== test_set_12
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 729b17729270dd87ccdf1bfe29b4e9bb
+DAUC 3G: opc = 228c2f2f06ac3268a9e616ee16db4ba1
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 311c4c929744d675b720f3b7e9b1cbd0
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 5380d158cfc30000f4e1436e9f67e4b2
+DAUC vector [0]: ck = 44c0f23c5493cfd241e48f197e1d1012
+DAUC vector [0]: ik = 0c9fb81613884c2535dd0eabf3b440d8
+DAUC vector [0]: res = 98dbbd099b3b408d0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 3c66cb98cab2d33d
+DAUC vector [0]: sres = 03e0fd84
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_12: SUCCESS
+
+
+===== test_set_13
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = d32dd23e89dc662354ca12eb79dd32fa
+DAUC 3G: opc = d22a4b4180a5325708a5ff70d9f67ec7
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = cf7d0ab1d94306950bf12018fbd46887
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 217af492728d00003bd338249751de80
+DAUC vector [0]: ck = 5af86b80edb70df5292cc1121cbad50c
+DAUC vector [0]: ik = 7f4d6ae7440e18789a8b75ad3f42f03a
+DAUC vector [0]: res = af4a411e1139f2c20000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 9612b5d88a4130bb
+DAUC vector [0]: sres = be73b3dc
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_13: SUCCESS
+
+
+===== test_set_14
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = af7c65e1927221de591187a2c5987a53
+DAUC 3G: opc = a4cf5c8155c08a7eff418e5443b98e55
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 1f0f8578464fd59b64bed2d09436b57a
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 837fd7b744390000557a836fd534e542
+DAUC vector [0]: ck = 3f8c3f3ccf7625bf77fc94bcfd22fd26
+DAUC vector [0]: ik = abcbae8fd46115e9961a55d0da5f2078
+DAUC vector [0]: res = 7bffa5c2f41fbc050000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 75a150df3c6aed08
+DAUC vector [0]: sres = 8fe019c7
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_14: SUCCESS
+
+
+===== test_set_15
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 5bd7ecd3d3127a41d12539bed4e7cf71
+DAUC 3G: opc = 76089d3c0ff3efdc6e36721d4fceb747
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 59b75f14251c75031d0bcbac1c2c04c7
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 5be11495527d0000298064f82a439924
+DAUC vector [0]: ck = d42b2d615e49a03ac275a5aef97af892
+DAUC vector [0]: ik = 0b3f8d024fe6bfafaa982b8f82e319c2
+DAUC vector [0]: res = 7e3f44c7591f6f450000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = b7f92e426a36fec5
+DAUC vector [0]: sres = 27202b82
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_15: SUCCESS
+
+
+===== test_set_16
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 6cd1c6ceb1e01e14f1b82316a90b7f3d
+DAUC 3G: opc = a219dc37f1dc7d66738b5843c799f206
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = f69b78f300a0568bce9f0cb93c4be4c9
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 1c408a858b1e0000e6e96310f83b5689
+DAUC vector [0]: ck = 6edaf99e5bd9f85d5f36d91c1272fb4b
+DAUC vector [0]: ik = d61c853c280dd9c46f297baec386de17
+DAUC vector [0]: res = 70f6bdb9ad21525f0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 88d9de10a22004c5
+DAUC vector [0]: sres = ddd7efe6
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_16: SUCCESS
+
+
+===== test_set_17
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = b73a90cbcf3afb622dba83c58a8415df
+DAUC 3G: opc = df0c67868fa25f748b7044c6e7c245b8
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = b120f1c1a0102a2f507dd543de68281f
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = aefdaa5dddb90000c4741d698b7a7ed3
+DAUC vector [0]: ck = 66195dbed0313274c5ca7766615fa25e
+DAUC vector [0]: ik = 66bec707eb2afc476d7408a8f2927b36
+DAUC vector [0]: res = 479dd25c20792d630000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = a819e577a8d6175b
+DAUC vector [0]: sres = 67e4ff3f
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_17: SUCCESS
+
+
+===== test_set_18
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 5122250214c33e723a5dd523fc145fc0
+DAUC 3G: opc = 981d464c7c52eb6e5036234984ad0bcf
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 81e92b6c0ee0e12ebceba8d92a99dfa5
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = ada15aeb7b980000a99729b59d5688b2
+DAUC vector [0]: ck = 5349fbe098649f948f5d2e973a81c00f
+DAUC vector [0]: ik = 9744871ad32bf9bbd1dd5ce54e3e2e5a
+DAUC vector [0]: res = 28d7b0f2a2ec3de50000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = 9a8d0e883ff0887a
+DAUC vector [0]: sres = 8a3b8d17
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_18: SUCCESS
+
+
+===== test_set_19
+aud3g.u.umts.sqn == 31
+DAUC Computing 1 auth vector: 3G only (2G derived from 3G keys)
+DAUC 3G: k = 90dca4eda45b53cf0f12d7c9c3bc6a89
+DAUC 3G: opc = cb9cccc4b9258e6dca4760379fb82581
+DAUC 3G: for sqn ind 0, previous sqn was 31
+DAUC vector [0]: rand = 9fddc72092c6ad036b6e464789315b78
+DAUC vector [0]: sqn = 32
+DAUC vector [0]: autn = 83cfd54db9330000695685b2b9214472
+DAUC vector [0]: ck = b5f2da03883b69f96bf52e029ed9ac45
+DAUC vector [0]: ik = b4721368bc16ea67875c5598688bb0ef
+DAUC vector [0]: res = a95100e2760952cd0000000000000000
+DAUC vector [0]: res_len = 8
+DAUC vector [0]: deriving 2G from 3G
+DAUC vector [0]: kc = ed29b2f1c27f9f34
+DAUC vector [0]: sres = df58522f
+DAUC vector [0]: auth_types = 0x3
+rc == 1
+aud3g.u.umts.sqn == 32
+vector matches expectations
+===== test_set_19: SUCCESS
+
diff --git a/tests/auc/auc_ts_55_205_test_sets.ok b/tests/auc/auc_ts_55_205_test_sets.ok
new file mode 100644
index 0000000..4eed389
--- /dev/null
+++ b/tests/auc/auc_ts_55_205_test_sets.ok
@@ -0,0 +1,2 @@
+3GPP TS 55.205 Test Sets
+Done
diff --git a/tests/auc/gen_ts_55_205_test_sets/Makefile.am b/tests/auc/gen_ts_55_205_test_sets/Makefile.am
new file mode 100644
index 0000000..3225384
--- /dev/null
+++ b/tests/auc/gen_ts_55_205_test_sets/Makefile.am
@@ -0,0 +1,6 @@
+EXTRA_DIST = \
+	func_template.c \
+	main_template.c \
+	pdftxt_2_c.py \
+	ts55_205_test_sets.txt \
+	$(NULL)
diff --git a/tests/auc/gen_ts_55_205_test_sets/func_template.c b/tests/auc/gen_ts_55_205_test_sets/func_template.c
new file mode 100644
index 0000000..0865432
--- /dev/null
+++ b/tests/auc/gen_ts_55_205_test_sets/func_template.c
@@ -0,0 +1,66 @@
+/* gen_ts_55_205_test_sets/func_template.c: Template to generate test code
+ * from 3GPP TS 55.205 test sets */
+
+/* (C) 2016 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 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/>.
+ *
+ */
+
+static void {func_name}(void)
+{{
+        struct osmo_sub_auth_data aud2g;
+        struct osmo_sub_auth_data aud3g;
+        struct osmo_auth_vector vec;
+        int rc;
+
+        comment_start();
+
+        aud2g = (struct osmo_sub_auth_data){{ 0 }};
+
+        aud3g = (struct osmo_sub_auth_data){{
+                .type = OSMO_AUTH_TYPE_UMTS,
+                .algo = OSMO_AUTH_ALG_MILENAGE,
+		.u.umts.sqn = 31,
+        }};
+
+        osmo_hexparse("{Ki}",
+                      aud3g.u.umts.k, sizeof(aud3g.u.umts.k));
+        osmo_hexparse("{OPc}",
+                      aud3g.u.umts.opc, sizeof(aud3g.u.umts.opc));
+
+        osmo_hexparse("{RAND}",
+                      fake_rand, sizeof(fake_rand));
+
+        vec = (struct osmo_auth_vector){{ {{0}} }};
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 31, "%"PRIu64);
+        rc = auc_compute_vectors(&vec, 1, &aud2g, &aud3g, NULL, NULL);
+        VERBOSE_ASSERT(rc, == 1, "%d");
+	VERBOSE_ASSERT(aud3g.u.umts.sqn, == 32, "%"PRIu64);
+
+        VEC_IS(&vec,
+               "  rand: {RAND}\n"
+               "  ck: {MIL3G-CK}\n"
+               "  ik: {MIL3G-IK}\n"
+               "  res: {MIL3G-RES}0000000000000000\n"
+               "  kc: {Kc}\n"
+               "  sres: {SRES#1}\n"
+              );
+
+	comment_end();
+}}
diff --git a/tests/auc/gen_ts_55_205_test_sets/main_template.c b/tests/auc/gen_ts_55_205_test_sets/main_template.c
new file mode 100644
index 0000000..a78bddb
--- /dev/null
+++ b/tests/auc/gen_ts_55_205_test_sets/main_template.c
@@ -0,0 +1,119 @@
+/* gen_ts_55_205_test_sets/main_template.c: Template to generate test code
+ * from 3GPP TS 55.205 test sets */
+
+/* (C) 2016 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 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 <string.h>
+#include <inttypes.h>
+
+#include <osmocom/core/application.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/msgb.h>
+
+#include <osmocom/crypt/auth.h>
+
+#include "logging.h"
+#include "auc.h"
+
+#define comment_start() fprintf(stderr, "\n===== %s\n", __func__);
+#define comment_end() fprintf(stderr, "===== %s: SUCCESS\n\n", __func__);
+
+#define VERBOSE_ASSERT(val, expect_op, fmt) \
+	do { \
+		fprintf(stderr, #val " == " fmt "\n", (val)); \
+		OSMO_ASSERT((val) expect_op); \
+	} while (0);
+
+char *vec_str(const struct osmo_auth_vector *vec)
+{
+	static char buf[1024];
+	char *pos = buf;
+	char *end = buf + sizeof(buf);
+
+#define append(what) \
+	if (pos >= end) \
+		return buf; \
+	pos += snprintf(pos, sizeof(buf) - (pos - buf), \
+                        "  " #what ": %s\n", \
+			osmo_hexdump_nospc((void*)&vec->what, sizeof(vec->what)))
+
+	append(rand);
+	append(ck);
+	append(ik);
+	append(res);
+	append(kc);
+	append(sres);
+#undef append
+
+	return buf;
+}
+
+#define VEC_IS(vec, expect) do { \
+		char *_is = vec_str(vec); \
+	        if (strcmp(_is, expect)) { \
+			fprintf(stderr, "MISMATCH! expected ==\n%s\n", \
+				expect); \
+			char *a = _is; \
+			char *b = expect; \
+			for (; *a && *b; a++, b++) { \
+				if (*a != *b) { \
+					while (a > _is && *(a-1) != '\n') a--; \
+					fprintf(stderr, "mismatch at %d:\n" \
+						"%s", (int)(a - _is), a); \
+					break; \
+				} \
+			} \
+			OSMO_ASSERT(false); \
+		} else \
+			fprintf(stderr, "vector matches expectations\n"); \
+	} while (0)
+
+uint8_t fake_rand[16] = { 0 };
+
+int rand_get(uint8_t *rand, unsigned int len)
+{
+	OSMO_ASSERT(len <= sizeof(fake_rand));
+	memcpy(rand, fake_rand, len);
+	return len;
+}
+
+FUNCTIONS
+
+int main()
+{
+	printf("3GPP TS 55.205 Test Sets\n");
+	void *tall_ctx = talloc_named_const(NULL, 1, "test");
+	msgb_talloc_ctx_init(tall_ctx, 0);
+	osmo_init_logging2(tall_ctx, &hlr_log_info);
+	log_set_print_filename(osmo_stderr_target, 0);
+	log_set_print_timestamp(osmo_stderr_target, 0);
+	log_set_use_color(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+	log_parse_category_mask(osmo_stderr_target, "DMAIN,1:DDB,1:DAUC,1");
+
+FUNCTION_CALLS
+
+	printf("Done\n");
+	return 0;
+}
diff --git a/tests/auc/gen_ts_55_205_test_sets/pdftxt_2_c.py b/tests/auc/gen_ts_55_205_test_sets/pdftxt_2_c.py
new file mode 100755
index 0000000..b01a797
--- /dev/null
+++ b/tests/auc/gen_ts_55_205_test_sets/pdftxt_2_c.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# FIXME: use python3 once buildslaves are updated.
+# Convert test sets pasted from 3GPP TS 55.205 to C code.
+ 
+# (C) 2016 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+#
+# All Rights Reserved
+#
+# Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+#
+# 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/>.
+
+import sys, os
+
+script_dir = sys.path[0]
+
+fields = (
+  'Ki',
+  'RAND',
+  'OP',
+  'OPc',
+  'MIL3G-RES',
+  'SRES#1',
+  'SRES#2',
+  'MIL3G-CK',
+  'MIL3G-IK',
+  'Kc',
+)
+
+test_sets_lines = []
+test_set_lines = None
+
+for line in [l.strip() for l in open(os.path.join(script_dir, 'ts55_205_test_sets.txt'), 'r')]:
+  if line.startswith('Test Set'):
+    if test_set_lines:
+      test_sets_lines.append(test_set_lines)
+    test_set_lines = []
+  elif len(line) == 8:
+    try:
+      is_hex = int(line, 16)
+      test_set_lines.append(line)
+    except ValueError:
+      pass
+
+if test_set_lines:
+  test_sets_lines.append(test_set_lines)
+
+# Magic fixups for PDF-to-text uselessness
+idx = (( 0, 10, 15, 19),
+       ( 1, 11, 16, 20),
+       ( 2, 12, 17, 21),
+       ( 3, 13, 18, 22),
+       ( 4, 14),
+       ( 5, ),
+       ( 6, ),
+       ( 7, 23, 26, 28),
+       ( 8, 24, 27, 29),
+       ( 9, 25 ),
+      )
+
+test_sets = []
+for l in test_sets_lines:
+  test_sets.append( [ ''.join([l[i] for i in li]) for li in idx ] )
+
+func_templ = open(os.path.join(script_dir, 'func_template.c'), 'r').read()
+
+funcs = []
+func_calls = []
+nr = 0
+for test_set in test_sets:
+  nr += 1
+  func_name = 'test_set_%d' % nr
+  kwargs = dict(zip(fields, test_set))
+  kwargs['func_name'] = func_name
+
+  func_calls.append('\t%s();' % func_name)
+  funcs.append(func_templ.format(**kwargs))
+
+templ = open(os.path.join(script_dir, 'main_template.c')).read()
+
+code = templ.replace('FUNCTIONS', '\n'.join(funcs)).replace('FUNCTION_CALLS', '\n'.join(func_calls))
+
+print('''
+/***** DO NOT EDIT THIS FILE -- THIS CODE IS GENERATED *****
+ ***** by gen_ts_55_205_test_sets/pdftxt_2_c.py        *****/
+''')
+print(code)
+
diff --git a/tests/auc/gen_ts_55_205_test_sets/ts55_205_test_sets.txt b/tests/auc/gen_ts_55_205_test_sets/ts55_205_test_sets.txt
new file mode 100644
index 0000000..0d4d14b
--- /dev/null
+++ b/tests/auc/gen_ts_55_205_test_sets/ts55_205_test_sets.txt
@@ -0,0 +1,972 @@
+
+Test Set 1
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+465b5ce8
+23553cbe
+cdc202d5
+cd63cb71
+a54211d5
+46f8416a
+a54211d5
+b40ba9a3
+f769bcd7
+eae4be82
+
+b199b49f
+9637a89d
+123e20f6
+954a9f4e
+e3ba50bf
+
+aa5f0a2e
+218ae64d
+2b6d676a
+48a5994e
+
+e238a6bc
+ae47bf35
+c72cb318
+37a02baf
+
+c58b2a05
+51044604
+3af9a08b
+
+bbf0d987
+12767271
+
+b21bf8cb
+1c6d3441
+
+Test Set 2
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+fec86ba6
+9f7c8d02
+dbc59adc
+1006020f
+8011c48c
+8c308a5e
+8011c48c
+5dbdbb29
+59a92d3b
+aa01739b
+
+eb707ed0
+1accf4db
+b6f9a0ef
+0a478bf6
+0c214ed2
+
+8905757b
+213ccff0
+735477b7
+b699f15c
+
+1bb44b8f
+c7f71a6a
+fadf8374
+062e42b3
+
+54e8f3cd
+476a0443
+8caa976d
+
+e665b046
+487055cf
+
+179a5098
+88b2307b
+
+ETSI
+
+3GPP TS 55.205 version 6.2.0 Release 6
+
+10
+
+ETSI TS 155 205 V6.2.0 (2006-03)
+
+Test Set 3
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+9e5944ae
+ce83dbc5
+223014c5
+a64a507a
+f365cd68
+cfbce3fe
+f365cd68
+e203edb3
+0c4524ad
+9a8ec95f
+
+a94b8116
+4ac0274a
+806694c0
+e1a2a98b
+3cd92e96
+
+5c82fbf9
+157c17f8
+07ca1eee
+b88eb421
+
+f32db751
+0d017bd6
+f57f004f
+0135dc87
+
+971574f5
+eac041c4
+408cc507
+
+a94b0d61
+dd830d20
+
+b816345d
+854fc46b
+
+Test Set 4
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+4ab1deb0
+74b0cd60
+2d16c5cd
+dcf07cbd
+5860fc1b
+9655e265
+5860fc1b
+7657766b
+1c42e960
+cdc1dc08
+
+5ca6ceb0
+31a1c833
+1fdf6b22
+51855290
+ce351e7e
+
+51fc98e7
+9b2b6ce2
+383584e3
+b92a07a9
+
+7d026a84
+b8c4a186
+bef2a8d8
+891e523e
+
+373d1c21
+d89b8fa9
+41b81a22
+
+38f307e3
+9f2744e0
+
+de9242f9
+708ccb53
+
+Test Set 5
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+6c38a116
+ee6466bc
+1ba00a1a
+3803ef53
+16c8233f
+13688f17
+16c8233f
+3f8c7587
+a7466cc1
+df75bc5e
+
+ac280c45
+96202c5a
+7c6700ac
+63b947c6
+05a0ac28
+
+4f59332e
+557abbef
+8c3ff3e9
+aaa225e5
+
+e35c8c4f
+f8babf63
+6ad08725
+8fae3934
+
+fe8e4b23
+e6b2a133
+a899879f
+
+3af676ae
+7d49d3b6
+
+de30ba3b
+6e95d7b4
+
+Test Set 6
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+2d609d4d
+194aa756
+460a4838
+c35a0ab0
+8c25a16c
+553d00b3
+8c25a16c
+4cd08460
+88ab80a4
+84b417ae
+
+b0ac5bf0
+013896b7
+5427aa39
+bcbfc925
+d918a1df
+
+d2c0de26
+4b4a2a3b
+264aac8e
+2caff15f
+
+7014de0d
+0af4539e
+fc9e73e8
+24efbde0
+
+20f8fa07
+15f15c73
+3aeab4f3
+
+31dd47cb
+711254a1
+
+dc6be411
+d388f696
+
+ETSI
+
+3GPP TS 55.205 version 6.2.0 Release 6
+
+11
+
+ETSI TS 155 205 V6.2.0 (2006-03)
+
+Test Set 7
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+a530a7fe
+3a4c2b32
+511c6c4e
+27953e49
+a63241e1
+59f1a44a
+a63241e1
+10f05bab
+f9ec0865
+3b4e244c
+
+428fad10
+45c50eb5
+83e38c89
+bc8af6dc
+ffc3e5ab
+
+82c45edd
+c71d0863
+b1c5d8dd
+c6e730eb
+
+fce13884
+9395764d
+e62426fa
+80286be3
+
+75a99a5f
+eb32f223
+dc60ce03
+
+bb98a9c2
+69cade40
+
+87679c3b
+c59c3a44
+
+Test Set 8
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+d9151cf0
+f761e5e9
+75fc2233
+c4c93eff
+4a90b217
+50588861
+4a90b217
+71236b71
+90527eba
+8d4ec01d
+
+4896e258
+3d603feb
+a44294ee
+e8a08138
+1ac83a76
+
+30bf2e08
+730e2755
+8e6de25c
+c203d4c2
+
+267b8360
+6cb8a2ca
+4353d26b
+7ce4e3d9
+
+29f9b22a
+a5588968
+e597acfe
+
+b77ea7a5
+db417273
+
+4c96da22
+25a04d9e
+
+Test Set 9
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+a0e2971b
+08eff828
+323792fa
+82a26f22
+4bc2212d
+cde6b027
+4bc2212d
+08cef6d0
+ed0318ca
+d8debc4f
+
+6822e8d3
+b13fdb56
+ca21fb4d
+bba9e948
+8624910a
+
+54a18cc2
+2722c65c
+5d6f13c1
+8f949a10
+
+35624ecb
+7f30a9b2
+45a9d2c1
+d98e9cc4
+
+04ec6147
+5deb9206
+fbcd60aa
+
+1a3c3cda
+272f6e8f
+
+048137fa
+a64ba411
+
+Test Set 10
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+0da6f7ba
+679ac4db
+4b9a26fa
+0db1071f
+6fc30fee
+02d13acd
+6fc30fee
+69b1cae7
+74f24e8c
+f0eaa50a
+
+86d5eac8
+acd7d233
+459e3acb
+8767562c
+6d123523
+
+a19cf563
+ff9d6806
+ff36f401
+a43a0a64
+
+ac58642d
+f4149ce3
+5de3bdc1
+c41e8d08
+
+c7429d97
+26df58e1
+1edcebb7
+
+5e245cac
+b38d7dcd
+
+b05a517c
+4f1b7fbd
+
+ETSI
+
+3GPP TS 55.205 version 6.2.0 Release 6
+
+12
+
+ETSI TS 155 205 V6.2.0 (2006-03)
+
+Test Set 11
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+77b45843
+4c47eb30
+bf3286c7
+d483afae
+aefa357b
+44389d01
+aefa357b
+908c43f0
+c251df0d
+82dbab7f
+
+c88e58c1
+76dc55fe
+a51409ce
+562409a3
+eac2a87a
+
+0d202684
+5106cb20
+95724d50
+26b5bb0b
+
+515ed430
+34b8cd78
+3bfe6e70
+20c4d762
+
+569cb8f7
+888dd932
+83f063da
+
+4bc971e7
+9bcf4665
+
+06c36c5f
+5b226e40
+
+Test Set 12
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+729b1772
+311c4c92
+d04c9c35
+228c2f2f
+98dbbd09
+03e0fd84
+98dbbd09
+44c0f23c
+0c9fb816
+3c66cb98
+
+9270dd87
+9744d675
+bd2262fa
+06ac3268
+9b3b408d
+
+ccdf1bfe
+b720f3b7
+810d2924
+a9e616ee
+
+29b4e9bb
+e9b1cbd0
+d036fd13
+16db4ba1
+
+5493cfd2
+13884c25
+cab2d33d
+
+41e48f19
+35dd0eab
+
+7e1d1012
+f3b440d8
+
+Test Set 13
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+d32dd23e
+cf7d0ab1
+fe75905b
+d22a4b41
+af4a411e
+be73b3dc
+af4a411e
+5af86b80
+7f4d6ae7
+9612b5d8
+
+89dc6623
+d9430695
+9da47d35
+80a53257
+1139f2c2
+
+54ca12eb
+0bf12018
+6236d031
+08a5ff70
+
+79dd32fa
+fbd46887
+4e09c32e
+d9f67ec7
+
+edb70df5
+440e1878
+8a4130bb
+
+292cc112
+9a8b75ad
+
+1cbad50c
+3f42f03a
+
+Test Set 14
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+af7c65e1
+1f0f8578
+0c7acb8d
+a4cf5c81
+7bffa5c2
+8fe019c7
+7bffa5c2
+3f8c3f3c
+abcbae8f
+75a150df
+
+927221de
+464fd59b
+95b7d4a3
+55c08a7e
+f41fbc05
+
+591187a2
+64bed2d0
+1c5aca6d
+ff418e54
+
+c5987a53
+9436b57a
+26345a88
+43b98e55
+
+cf7625bf
+d46115e9
+3c6aed08
+
+77fc94bc
+961a55d0
+
+fd22fd26
+da5f2078
+
+ETSI
+
+3GPP TS 55.205 version 6.2.0 Release 6
+
+13
+
+ETSI TS 155 205 V6.2.0 (2006-03)
+
+Test Set 15
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+5bd7ecd3
+59b75f14
+f967f760
+76089d3c
+7e3f44c7
+27202b82
+7e3f44c7
+d42b2d61
+0b3f8d02
+b7f92e42
+
+d3127a41
+251c7503
+38b920a9
+0ff3efdc
+591f6f45
+
+d12539be
+1d0bcbac
+cd25e10c
+6e36721d
+
+d4e7cf71
+1c2c04c7
+08b49924
+4fceb747
+
+5e49a03a
+4fe6bfaf
+6a36fec5
+
+c275a5ae
+aa982b8f
+
+f97af892
+82e319c2
+
+Test Set 16
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+6cd1c6ce
+f69b78f3
+078bfca9
+a219dc37
+70f6bdb9
+ddd7efe6
+70f6bdb9
+6edaf99e
+d61c853c
+88d9de10
+
+b1e01e14
+00a0568b
+564659ec
+f1dc7d66
+ad21525f
+
+f1b82316
+ce9f0cb9
+d8851e84
+738b5843
+
+a90b7f3d
+3c4be4c9
+e6c59b48
+c799f206
+
+5bd9f85d
+280dd9c4
+a22004c5
+
+5f36d91c
+6f297bae
+
+1272fb4b
+c386de17
+
+Test Set 17
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+b73a90cb
+b120f1c1
+b672047e
+df0c6786
+479dd25c
+67e4ff3f
+479dd25c
+66195dbe
+66bec707
+a819e577
+
+cf3afb62
+a0102a2f
+003bb952
+8fa25f74
+20792d63
+
+2dba83c5
+507dd543
+dca6cb8a
+8b7044c6
+
+8a8415df
+de68281f
+f0e5b779
+e7c245b8
+
+d0313274
+eb2afc47
+a8d6175b
+
+c5ca7766
+6d7408a8
+
+615fa25e
+f2927b36
+
+Test Set 18
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+51222502
+81e92b6c
+c9e87632
+981d464c
+28d7b0f2
+8a3b8d17
+28d7b0f2
+5349fbe0
+9744871a
+9a8d0e88
+
+14c33e72
+0ee0e12e
+86b5b9ff
+7c52eb6e
+a2ec3de5
+
+3a5dd523
+bceba8d9
+bdf56e12
+50362349
+
+fc145fc0
+2a99dfa5
+97d0887b
+84ad0bcf
+
+98649f94
+d32bf9bb
+3ff0887a
+
+8f5d2e97
+d1dd5ce5
+
+3a81c00f
+4e3e2e5a
+
+ETSI
+
+3GPP TS 55.205 version 6.2.0 Release 6
+
+Test Set 19
+Ki
+RAND
+OP
+OPc
+MIL3G-RES
+SRES#1
+SRES#2
+MIL3G-CK
+MIL3G-IK
+Kc
+
+90dca4ed
+9fddc720
+3ffcfe5b
+cb9cccc4
+a95100e2
+df58522f
+a95100e2
+b5f2da03
+b4721368
+ed29b2f1
+
+14
+
+ETSI TS 155 205 V6.2.0 (2006-03)
+
+a45b53cf
+92c6ad03
+7b111158
+b9258e6d
+760952cd
+
+0f12d7c9
+6b6e4647
+9920d352
+ca476037
+
+c3bc6a89
+89315b78
+8e84e655
+9fb82581
+
+883b69f9
+bc16ea67
+c27f9f34
+
+6bf52e02
+875c5598
+
+9ed9ac45
+688bb0ef
+
diff --git a/tests/db/Makefile.am b/tests/db/Makefile.am
new file mode 100644
index 0000000..d5fce65
--- /dev/null
+++ b/tests/db/Makefile.am
@@ -0,0 +1,50 @@
+AM_CFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir)/src \
+	-I$(top_builddir)/src \
+	-Wall \
+	-ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(LIBOSMOABIS_CFLAGS) \
+	$(SQLITE3_CFLAGS) \
+	$(NULL)
+
+EXTRA_DIST = \
+	db_test.ok \
+	db_test.err \
+	$(NULL)
+
+check_PROGRAMS = db_test
+
+db_test_SOURCES = \
+	db_test.c \
+	$(NULL)
+
+db_test_LDADD = \
+	$(top_srcdir)/src/db.c \
+	$(top_srcdir)/src/db_hlr.c \
+	$(top_srcdir)/src/db_auc.c \
+	$(top_srcdir)/src/logging.c \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(LIBOSMOABIS_LIBS) \
+	$(SQLITE3_LIBS) \
+	$(NULL)
+
+.PHONY: db_test.db update_exp manual manual-nonverbose manual-gdb
+db_test.db:
+	rm -f db_test.db
+	sqlite3 $(builddir)/db_test.db < $(top_srcdir)/sql/hlr.sql
+
+update_exp: db_test.db
+	cd $(builddir); ./db_test >"$(srcdir)/db_test.ok" 2>"$(srcdir)/db_test.err"
+
+manual: db_test.db
+	cd $(builddir); ./db_test -v
+
+manual-nonverbose: db_test.db
+	cd $(builddir); ./db_test
+
+manual-gdb: db_test.db
+	cd $(builddir); gdb -ex run --args ./db_test -v
diff --git a/tests/db/db_test.c b/tests/db/db_test.c
new file mode 100644
index 0000000..058588b
--- /dev/null
+++ b/tests/db/db_test.c
@@ -0,0 +1,871 @@
+/* (C) 2017 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 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 <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+
+#include <osmocom/core/application.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/logging.h>
+
+#include "db.h"
+#include "logging.h"
+
+#define comment_start() fprintf(stderr, "\n===== %s\n", __func__);
+#define comment(fmt, args...) fprintf(stderr, "\n--- " fmt "\n\n", ## args);
+#define comment_end() fprintf(stderr, "===== %s: SUCCESS\n\n", __func__);
+
+#define fill_invalid(x) _fill_invalid(&x, sizeof(x))
+static void _fill_invalid(void *dest, size_t size)
+{
+	uint8_t *pos = dest;
+	size_t remain = size;
+	int wrote = 0;
+	do {
+		remain -= wrote;
+		pos += wrote;
+		wrote = snprintf((void*)pos, remain, "-invalid-data");
+	} while (wrote < remain);
+}
+
+/* Perform a function call and verbosely assert that its return value is as expected.
+ * The return code is then available in g_rc. */
+#define ASSERT_RC(call, expect_rc) \
+	do { \
+		fprintf(stderr, #call " --> " #expect_rc "\n"); \
+		g_rc = call; \
+		if (g_rc != (expect_rc)) \
+			fprintf(stderr, " MISMATCH: got rc = %d, expected: " \
+                                        #expect_rc " = %d\n", g_rc, expect_rc); \
+		OSMO_ASSERT(g_rc == (expect_rc)); \
+		fprintf(stderr, "\n"); \
+	} while (0)
+
+/* Do db_subscr_get_by_xxxx and verbosely assert that its return value is as expected.
+ * Print the subscriber struct to stderr to be validated by db_test.err.
+ * The result is then available in g_subscr. */
+#define ASSERT_SEL(by, val, expect_rc) \
+	do { \
+		int rc; \
+		fill_invalid(g_subscr); \
+		fprintf(stderr, "db_subscr_get_by_" #by "(dbc, " #val ", &g_subscr) --> " \
+                                #expect_rc "\n"); \
+		rc = db_subscr_get_by_##by(dbc, val, &g_subscr); \
+		if (rc != (expect_rc)) \
+			fprintf(stderr, " MISMATCH: got rc = %d, expected: " \
+                                        #expect_rc " = %d\n", rc, expect_rc); \
+		OSMO_ASSERT(rc == (expect_rc)); \
+		if (!rc) \
+			dump_subscr(&g_subscr); \
+		fprintf(stderr, "\n"); \
+	} while (0)
+
+/* Do db_get_auth_data() and verbosely assert that its return value is as expected.
+ * Print the subscriber struct to stderr to be validated by db_test.err.
+ * The results are then available in g_aud2g and g_aud3g. */
+#define ASSERT_SEL_AUD(imsi, expect_rc, expect_id) \
+	do { \
+		fill_invalid(g_aud2g); \
+		fill_invalid(g_aud3g); \
+		g_id = 0; \
+		ASSERT_RC(db_get_auth_data(dbc, imsi, &g_aud2g, &g_aud3g, &g_id), expect_rc); \
+		if (!g_rc) { \
+			dump_aud("2G", &g_aud2g); \
+			dump_aud("3G", &g_aud3g); \
+		}\
+		if (g_id != expect_id) {\
+			fprintf(stderr, "MISMATCH: got subscriber id %"PRId64 \
+				", expected %"PRId64"\n", g_id, (int64_t)(expect_id)); \
+			OSMO_ASSERT(g_id == expect_id); \
+		} \
+		fprintf(stderr, "\n"); \
+	} while (0)
+
+#define N_VECTORS 3
+
+#define ASSERT_DB_GET_AUC(imsi, expect_rc) \
+	do { \
+		struct osmo_auth_vector vec[N_VECTORS]; \
+		ASSERT_RC(db_get_auc(dbc, imsi, 3, vec, N_VECTORS, NULL, NULL), expect_rc); \
+	} while (0)
+
+/* Not linking the real auc_compute_vectors(), just returning num_vec.
+ * This gets called by db_get_auc(), but we're only interested in its rc. */
+int auc_compute_vectors(struct osmo_auth_vector *vec, unsigned int num_vec,
+			struct osmo_sub_auth_data *aud2g,
+			struct osmo_sub_auth_data *aud3g,
+			const uint8_t *rand_auts, const uint8_t *auts)
+{ return num_vec; }
+
+static struct db_context *dbc = NULL;
+static void *ctx = NULL;
+static struct hlr_subscriber g_subscr;
+static struct osmo_sub_auth_data g_aud2g;
+static struct osmo_sub_auth_data g_aud3g;
+static int g_rc;
+static int64_t g_id;
+
+#define Pfv(name, fmt, val) \
+	fprintf(stderr, "  ." #name " = " fmt ",\n", val)
+#define Pfo(name, fmt, obj) \
+	Pfv(name, fmt, obj->name)
+
+/* Print a subscriber struct to stderr to be validated by db_test.err. */
+void dump_subscr(struct hlr_subscriber *subscr)
+{
+#define Ps(name) \
+	if (*subscr->name) \
+		Pfo(name, "'%s'", subscr)
+#define Pd(name) \
+	Pfv(name, "%"PRId64, (int64_t)subscr->name)
+#define Pd_nonzero(name) \
+	if (subscr->name) \
+		Pd(name)
+#define Pb(if_val, name) \
+	if (subscr->name == (if_val)) \
+		Pfv(name, "%s", subscr->name ? "true" : "false")
+
+	fprintf(stderr, "struct hlr_subscriber {\n");
+	Pd(id);
+	Ps(imsi);
+	Ps(msisdn);
+	Ps(vlr_number);
+	Ps(sgsn_number);
+	Ps(sgsn_address);
+	Pd_nonzero(periodic_lu_timer);
+	Pd_nonzero(periodic_rau_tau_timer);
+	Pb(false, nam_cs);
+	Pb(false, nam_ps);
+	if (subscr->lmsi)
+		Pfo(lmsi, "0x%x", subscr);
+	Pb(true, ms_purged_cs);
+	Pb(true, ms_purged_ps);
+	fprintf(stderr, "}\n");
+#undef Ps
+#undef Pd
+#undef Pd_nonzero
+#undef Pb
+}
+
+void dump_aud(const char *label, struct osmo_sub_auth_data *aud)
+{
+	if (aud->type == OSMO_AUTH_TYPE_NONE) {
+		fprintf(stderr, "%s: none\n", label);
+		return;
+	}
+
+	fprintf(stderr, "%s: struct osmo_sub_auth_data {\n", label);
+#define Pf(name, fmt) \
+	Pfo(name, fmt, aud)
+#define Phex(name) \
+	Pfv(name, "'%s'", osmo_hexdump_nospc(aud->name, sizeof(aud->name)))
+
+	Pfv(type, "%s", osmo_sub_auth_type_name(aud->type));
+	Pfv(algo, "%s", osmo_auth_alg_name(aud->algo));
+	switch (aud->type) {
+	case OSMO_AUTH_TYPE_GSM:
+		Phex(u.gsm.ki);
+		break;
+	case OSMO_AUTH_TYPE_UMTS:
+		Phex(u.umts.opc);
+		Pf(u.umts.opc_is_op, "%u");
+		Phex(u.umts.k);
+		Phex(u.umts.amf);
+		if (aud->u.umts.sqn) {
+			Pf(u.umts.sqn, "%"PRIu64);
+			Pf(u.umts.sqn, "0x%"PRIx64);
+		}
+		if (aud->u.umts.ind_bitlen)
+			Pf(u.umts.ind_bitlen, "%u");
+		break;
+	default:
+		OSMO_ASSERT(false);
+	}
+
+	fprintf(stderr, "}\n");
+
+#undef Pf
+#undef Phex
+}
+
+static const char *imsi0 = "123456789000000";
+static const char *imsi1 = "123456789000001";
+static const char *imsi2 = "123456789000002";
+static const char *short_imsi = "123456";
+static const char *unknown_imsi = "999999999";
+
+static void test_subscr_create_update_sel_delete()
+{
+	int64_t id0, id1, id2, id_short;
+	comment_start();
+
+	comment("Create with valid / invalid IMSI");
+
+	ASSERT_RC(db_subscr_create(dbc, imsi0), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	id0 = g_subscr.id;
+	ASSERT_RC(db_subscr_create(dbc, imsi1), 0);
+	ASSERT_SEL(imsi, imsi1, 0);
+	id1 = g_subscr.id;
+	ASSERT_RC(db_subscr_create(dbc, imsi2), 0);
+	ASSERT_SEL(imsi, imsi2, 0);
+	id2 = g_subscr.id;
+	ASSERT_RC(db_subscr_create(dbc, imsi0), -EIO);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_create(dbc, imsi1), -EIO);
+	ASSERT_RC(db_subscr_create(dbc, imsi1), -EIO);
+	ASSERT_SEL(imsi, imsi1, 0);
+	ASSERT_RC(db_subscr_create(dbc, imsi2), -EIO);
+	ASSERT_RC(db_subscr_create(dbc, imsi2), -EIO);
+	ASSERT_SEL(imsi, imsi2, 0);
+
+	ASSERT_RC(db_subscr_create(dbc, "123456789 000003"), -EINVAL);
+	ASSERT_SEL(imsi, "123456789000003", -ENOENT);
+
+	ASSERT_RC(db_subscr_create(dbc, "123456789000002123456"), -EINVAL);
+	ASSERT_SEL(imsi, "123456789000002123456", -ENOENT);
+
+	ASSERT_RC(db_subscr_create(dbc, "foobar123"), -EINVAL);
+	ASSERT_SEL(imsi, "foobar123", -ENOENT);
+
+	ASSERT_RC(db_subscr_create(dbc, "123"), -EINVAL);
+	ASSERT_SEL(imsi, "123", -ENOENT);
+
+	ASSERT_RC(db_subscr_create(dbc, short_imsi), 0);
+	ASSERT_SEL(imsi, short_imsi, 0);
+	id_short = g_subscr.id;
+
+
+	comment("Set valid / invalid MSISDN");
+
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0, "54321"), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "54321", 0);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0,
+					  "54321012345678912345678"), -EINVAL);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "54321", 0);
+	ASSERT_SEL(msisdn, "54321012345678912345678", -ENOENT);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0,
+					  "543 21"), -EINVAL);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "543 21", -ENOENT);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0,
+					  "foobar123"), -EINVAL);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "foobar123", -ENOENT);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0,
+					  "5"), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "5", 0);
+	ASSERT_SEL(msisdn, "54321", -ENOENT);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0,
+					  "543210123456789"), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "543210123456789", 0);
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, imsi0,
+					  "5432101234567891"), -EINVAL);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_SEL(msisdn, "5432101234567891", -ENOENT);
+
+	comment("Set MSISDN on non-existent / invalid IMSI");
+
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, unknown_imsi, "99"), -ENOENT);
+	ASSERT_SEL(msisdn, "99", -ENOENT);
+
+	ASSERT_RC(db_subscr_update_msisdn_by_imsi(dbc, "foobar", "99"), -ENOENT);
+	ASSERT_SEL(msisdn, "99", -ENOENT);
+
+	comment("Set / unset nam_cs and nam_ps");
+
+	/*                                nam_val, is_ps */
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, false, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, false, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, true, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, true, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	comment("Set / unset nam_cs and nam_ps *again*");
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, false, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, false, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, false, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, false, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, true, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, true, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, true, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_nam(dbc, imsi0, true, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	comment("Set nam_cs and nam_ps on non-existent / invalid IMSI");
+
+	ASSERT_RC(db_subscr_nam(dbc, unknown_imsi, false, true), -ENOENT);
+	ASSERT_RC(db_subscr_nam(dbc, unknown_imsi, false, false), -ENOENT);
+	ASSERT_SEL(imsi, unknown_imsi, -ENOENT);
+
+	ASSERT_RC(db_subscr_nam(dbc, "foobar", false, true), -ENOENT);
+	ASSERT_RC(db_subscr_nam(dbc, "foobar", false, false), -ENOENT);
+
+	comment("Record LU for PS and CS (SGSN and VLR names)");
+
+	ASSERT_RC(db_subscr_lu(dbc, id0, "5952", true), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, "712", false), 0);
+	ASSERT_SEL(id, id0, 0);
+
+	comment("Record LU for PS and CS (SGSN and VLR names) *again*");
+
+	ASSERT_RC(db_subscr_lu(dbc, id0, "111", true), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, "111", true), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, "222", false), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, "222", false), 0);
+	ASSERT_SEL(id, id0, 0);
+
+	comment("Unset LU info for PS and CS (SGSN and VLR names)");
+	ASSERT_RC(db_subscr_lu(dbc, id0, "", true), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, "", false), 0);
+	ASSERT_SEL(id, id0, 0);
+
+	ASSERT_RC(db_subscr_lu(dbc, id0, "111", true), 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, "222", false), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, NULL, true), 0);
+	ASSERT_SEL(id, id0, 0);
+	ASSERT_RC(db_subscr_lu(dbc, id0, NULL, false), 0);
+	ASSERT_SEL(id, id0, 0);
+
+	comment("Record LU for non-existent ID");
+	ASSERT_RC(db_subscr_lu(dbc, 99999, "5952", true), -ENOENT);
+	ASSERT_RC(db_subscr_lu(dbc, 99999, "712", false), -ENOENT);
+	ASSERT_SEL(id, 99999, -ENOENT);
+
+	comment("Purge and un-purge PS and CS");
+
+	/*                               purge_val, is_ps */
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, true, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, true, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, false, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, false, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	comment("Purge PS and CS *again*");
+
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, true, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, true, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, false, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, false, true), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, true, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, true, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, false, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_purge(dbc, imsi0, false, false), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	comment("Purge on non-existent / invalid IMSI");
+
+	ASSERT_RC(db_subscr_purge(dbc, unknown_imsi, true, true), -ENOENT);
+	ASSERT_SEL(imsi, unknown_imsi, -ENOENT);
+	ASSERT_RC(db_subscr_purge(dbc, unknown_imsi, true, false), -ENOENT);
+	ASSERT_SEL(imsi, unknown_imsi, -ENOENT);
+
+	comment("Delete non-existent / invalid IDs");
+
+	ASSERT_RC(db_subscr_delete_by_id(dbc, 999), -ENOENT);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, -10), -ENOENT);
+
+	comment("Delete subscribers");
+
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id0), 0);
+	ASSERT_SEL(imsi, imsi0, -ENOENT);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id0), -ENOENT);
+
+	ASSERT_SEL(imsi, imsi1, 0);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id1), 0);
+	ASSERT_SEL(imsi, imsi1, -ENOENT);
+
+	ASSERT_SEL(imsi, imsi2, 0);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id2), 0);
+	ASSERT_SEL(imsi, imsi2, -ENOENT);
+
+	ASSERT_SEL(imsi, short_imsi, 0);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id_short), 0);
+	ASSERT_SEL(imsi, short_imsi, -ENOENT);
+
+	comment_end();
+}
+
+static const struct sub_auth_data_str *mk_aud_2g(enum osmo_auth_algo algo,
+						 const char *ki)
+{
+	static struct sub_auth_data_str aud;
+	aud = (struct sub_auth_data_str){
+		.type = OSMO_AUTH_TYPE_GSM,
+		.algo = algo,
+		.u.gsm.ki = ki,
+	};
+	return &aud;
+}
+
+static const struct sub_auth_data_str *mk_aud_3g(enum osmo_auth_algo algo,
+						 const char *opc, bool opc_is_op,
+						 const char *k, unsigned int ind_bitlen)
+{
+	static struct sub_auth_data_str aud;
+	aud = (struct sub_auth_data_str){
+		.type = OSMO_AUTH_TYPE_UMTS,
+		.algo = algo,
+		.u.umts.k = k,
+		.u.umts.opc = opc,
+		.u.umts.opc_is_op = opc_is_op ? 1 : 0,
+		.u.umts.ind_bitlen = ind_bitlen,
+	};
+	return &aud;
+}
+
+static void test_subscr_aud()
+{
+	int64_t id;
+
+	comment_start();
+
+	comment("Get auth data for non-existent subscriber");
+	ASSERT_SEL_AUD(unknown_imsi, -ENOENT, 0);
+	ASSERT_DB_GET_AUC(imsi0, -ENOENT);
+
+	comment("Create subscriber");
+
+	ASSERT_RC(db_subscr_create(dbc, imsi0), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	id = g_subscr.id;
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+	ASSERT_DB_GET_AUC(imsi0, -ENOKEY);
+
+
+	comment("Set auth data, 2G only");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_COMP128v1, "0123456789abcdef0123456789abcdef")),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+	ASSERT_DB_GET_AUC(imsi0, N_VECTORS);
+
+	/* same again */
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_COMP128v1, "0123456789abcdef0123456789abcdef")),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_COMP128v2, "BeadedBeeAced1EbbedDefacedFacade")),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_COMP128v3, "DeafBeddedBabeAcceededFadedDecaf")),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_XOR, "CededEffacedAceFacedBadFadedBeef")),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	comment("Remove 2G auth data");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_NONE, NULL)),
+		0);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+	ASSERT_DB_GET_AUC(imsi0, -ENOKEY);
+
+	/* Removing nothing results in -ENOENT */
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_NONE, NULL)),
+		-ENOENT);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_XOR, "CededEffacedAceFacedBadFadedBeef")),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_NONE, "f000000000000f00000000000f000000")),
+		0);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+	ASSERT_DB_GET_AUC(imsi0, -ENOKEY);
+
+
+	comment("Set auth data, 3G only");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "BeefedCafeFaceAcedAddedDecadeFee", true,
+			  "C01ffedC1cadaeAc1d1f1edAcac1aB0a", 5)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+	ASSERT_DB_GET_AUC(imsi0, N_VECTORS);
+
+	/* same again */
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "BeefedCafeFaceAcedAddedDecadeFee", true,
+			  "C01ffedC1cadaeAc1d1f1edAcac1aB0a", 5)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "Deaf0ff1ceD0d0DabbedD1ced1ceF00d", true,
+			  "F1bbed0afD0eF0bD0ffed0ddF1fe0b0e", 0)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "BeefedCafeFaceAcedAddedDecadeFee", false,
+			  "DeafBeddedBabeAcceededFadedDecaf",
+			  OSMO_MILENAGE_IND_BITLEN_MAX)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "CededEffacedAceFacedBadFadedBeef", false,
+			  "BeefedCafeFaceAcedAddedDecadeFee", 5)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	comment("Remove 3G auth data");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_NONE, NULL, false, NULL, 0)),
+		0);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+	ASSERT_DB_GET_AUC(imsi0, -ENOKEY);
+
+	/* Removing nothing results in -ENOENT */
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_NONE, NULL, false, NULL, 0)),
+		-ENOENT);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "CededEffacedAceFacedBadFadedBeef", false,
+			  "BeefedCafeFaceAcedAddedDecadeFee", 5)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+	ASSERT_DB_GET_AUC(imsi0, N_VECTORS);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_NONE,
+			  "asdfasdfasd", false,
+			  "asdfasdfasdf", 99999)),
+		0);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+	ASSERT_DB_GET_AUC(imsi0, -ENOKEY);
+
+
+	comment("Set auth data, 2G and 3G");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_COMP128v3, "CededEffacedAceFacedBadFadedBeef")),
+		0);
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "BeefedCafeFaceAcedAddedDecadeFee", false,
+			  "DeafBeddedBabeAcceededFadedDecaf", 5)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+	ASSERT_DB_GET_AUC(imsi0, N_VECTORS);
+
+
+	comment("Set invalid auth data");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(99999, "f000000000000f00000000000f000000")),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_XOR, "f000000000000f00000000000f000000f00000000")),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_XOR, "f00")),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_2g(OSMO_AUTH_ALG_MILENAGE, "0123456789abcdef0123456789abcdef")),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "0f000000000000f00000000000f000000", false,
+			  "f000000000000f00000000000f000000", 5)),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "f000000000000f00000000000f000000", false,
+			  "000000000000f00000000000f000000", 5)),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "f000000000000f00000000000f000000", false,
+			  "f000000000000f00000000000f000000",
+			  OSMO_MILENAGE_IND_BITLEN_MAX + 1)),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "X000000000000f00000000000f000000", false,
+			  "f000000000000f00000000000f000000", 5)),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "f000000000000f00000000000f000000", false,
+			  "f000000000000 f00000000000 f000000", 5)),
+		-EINVAL);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	comment("Delete subscriber");
+
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id), 0);
+	ASSERT_SEL(imsi, imsi0, -ENOENT);
+
+	comment("Re-add subscriber and verify auth data didn't come back");
+
+	ASSERT_RC(db_subscr_create(dbc, imsi0), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	/* For this test to work, we want to get the same subscriber ID back,
+	 * and make sure there are no auth data leftovers for this ID. */
+	OSMO_ASSERT(id == g_subscr.id);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+	ASSERT_DB_GET_AUC(imsi0, -ENOKEY);
+
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id), 0);
+	ASSERT_SEL(imsi, imsi0, -ENOENT);
+	ASSERT_DB_GET_AUC(imsi0, -ENOENT);
+
+	comment_end();
+}
+
+static void test_subscr_sqn()
+{
+	int64_t id;
+
+	comment_start();
+
+	comment("Set SQN for unknown subscriber");
+
+	ASSERT_RC(db_update_sqn(dbc, 99, 999), -ENOENT);
+	ASSERT_SEL(id, 99, -ENOENT);
+
+	ASSERT_RC(db_update_sqn(dbc, 9999, 99), -ENOENT);
+	ASSERT_SEL(id, 9999, -ENOENT);
+
+	comment("Create subscriber");
+
+	ASSERT_RC(db_subscr_create(dbc, imsi0), 0);
+	ASSERT_SEL(imsi, imsi0, 0);
+
+	id = g_subscr.id;
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+
+	comment("Set SQN, but no 3G auth data present");
+
+	ASSERT_RC(db_update_sqn(dbc, id, 123), -ENOENT);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+
+	ASSERT_RC(db_update_sqn(dbc, id, 543), -ENOENT);
+	ASSERT_SEL_AUD(imsi0, -ENOKEY, id);
+
+	comment("Set auth 3G data");
+
+	ASSERT_RC(db_subscr_update_aud_by_id(dbc, id,
+		mk_aud_3g(OSMO_AUTH_ALG_MILENAGE,
+			  "BeefedCafeFaceAcedAddedDecadeFee", true,
+			  "C01ffedC1cadaeAc1d1f1edAcac1aB0a", 5)),
+		0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	comment("Set SQN");
+
+	ASSERT_RC(db_update_sqn(dbc, id, 23315), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_update_sqn(dbc, id, 23315), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_update_sqn(dbc, id, 423), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	comment("Set SQN: thru uint64_t range, using the int64_t SQLite bind");
+
+	ASSERT_RC(db_update_sqn(dbc, id, 0), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_update_sqn(dbc, id, INT64_MAX), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_update_sqn(dbc, id, INT64_MIN), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	ASSERT_RC(db_update_sqn(dbc, id, UINT64_MAX), 0);
+	ASSERT_SEL_AUD(imsi0, 0, id);
+
+	comment("Delete subscriber");
+
+	ASSERT_SEL(imsi, imsi0, 0);
+	ASSERT_RC(db_subscr_delete_by_id(dbc, id), 0);
+	ASSERT_SEL(imsi, imsi0, -ENOENT);
+
+	comment_end();
+}
+
+static struct {
+	bool verbose;
+} cmdline_opts = {
+	.verbose = false,
+};
+
+static void print_help(const char *program)
+{
+	printf("Usage:\n"
+	       "  %s [-v] [N [N...]]\n"
+	       "Options:\n"
+	       "  -h --help      show this text.\n"
+	       "  -v --verbose   print source file and line numbers\n",
+	       program
+	       );
+}
+
+static void handle_options(int argc, char **argv)
+{
+	while (1) {
+		int option_index = 0, c;
+		static struct option long_options[] = {
+			{"help", 0, 0, 'h'},
+			{"verbose", 1, 0, 'v'},
+			{0, 0, 0, 0}
+		};
+
+		c = getopt_long(argc, argv, "hv",
+				long_options, &option_index);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			print_help(argv[0]);
+			exit(0);
+		case 'v':
+			cmdline_opts.verbose = true;
+			break;
+		default:
+			/* catch unknown options *as well as* missing arguments. */
+			fprintf(stderr, "Error in command line options. Exiting.\n");
+			exit(-1);
+			break;
+		}
+	}
+
+	if (optind < argc) {
+		fprintf(stderr, "too many args\n");
+		exit(-1);
+	}
+}
+
+int main(int argc, char **argv)
+{
+	printf("db_test.c\n");
+
+	ctx = talloc_named_const(NULL, 1, "db_test");
+
+	handle_options(argc, argv);
+
+	osmo_init_logging2(ctx, &hlr_log_info);
+	log_set_print_filename(osmo_stderr_target, cmdline_opts.verbose);
+	log_set_print_timestamp(osmo_stderr_target, 0);
+	log_set_use_color(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+	log_parse_category_mask(osmo_stderr_target, "DMAIN,1:DDB,1:DAUC,1");
+
+	/* omit the SQLite version and compilation flags from test output */
+	log_set_log_level(osmo_stderr_target, LOGL_ERROR);
+	/* Disable SQLite logging so that we're not vulnerable on SQLite error messages changing across
+	 * library versions. */
+	dbc = db_open(ctx, "db_test.db", false);
+	log_set_log_level(osmo_stderr_target, 0);
+	OSMO_ASSERT(dbc);
+
+	test_subscr_create_update_sel_delete();
+	test_subscr_aud();
+	test_subscr_sqn();
+
+	printf("Done\n");
+	return 0;
+}
+
+/* stubs */
+void *lu_op_alloc_conn(void *conn)
+{ OSMO_ASSERT(false); return NULL; }
+void lu_op_tx_del_subscr_data(void *luop)
+{ OSMO_ASSERT(false); }
+void lu_op_free(void *luop)
+{ OSMO_ASSERT(false); }
diff --git a/tests/db/db_test.err b/tests/db/db_test.err
new file mode 100644
index 0000000..1d34045
--- /dev/null
+++ b/tests/db/db_test.err
@@ -0,0 +1,1457 @@
+
+===== test_subscr_create_update_sel_delete
+
+--- Create with valid / invalid IMSI
+
+db_subscr_create(dbc, imsi0) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_subscr_create(dbc, imsi1) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi1, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 2,
+  .imsi = '123456789000001',
+}
+
+db_subscr_create(dbc, imsi2) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi2, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 3,
+  .imsi = '123456789000002',
+}
+
+db_subscr_create(dbc, imsi0) --> -EIO
+DAUC IMSI='123456789000000': Cannot create subscriber: SQL error: (2067) UNIQUE constraint failed: subscriber.imsi
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_subscr_create(dbc, imsi1) --> -EIO
+DAUC IMSI='123456789000001': Cannot create subscriber: SQL error: (2067) UNIQUE constraint failed: subscriber.imsi
+
+db_subscr_create(dbc, imsi1) --> -EIO
+DAUC IMSI='123456789000001': Cannot create subscriber: SQL error: (2067) UNIQUE constraint failed: subscriber.imsi
+
+db_subscr_get_by_imsi(dbc, imsi1, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 2,
+  .imsi = '123456789000001',
+}
+
+db_subscr_create(dbc, imsi2) --> -EIO
+DAUC IMSI='123456789000002': Cannot create subscriber: SQL error: (2067) UNIQUE constraint failed: subscriber.imsi
+
+db_subscr_create(dbc, imsi2) --> -EIO
+DAUC IMSI='123456789000002': Cannot create subscriber: SQL error: (2067) UNIQUE constraint failed: subscriber.imsi
+
+db_subscr_get_by_imsi(dbc, imsi2, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 3,
+  .imsi = '123456789000002',
+}
+
+db_subscr_create(dbc, "123456789 000003") --> -EINVAL
+DAUC Cannot create subscriber: invalid IMSI: '123456789 000003'
+
+db_subscr_get_by_imsi(dbc, "123456789000003", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000003': No such subscriber
+
+db_subscr_create(dbc, "123456789000002123456") --> -EINVAL
+DAUC Cannot create subscriber: invalid IMSI: '123456789000002123456'
+
+db_subscr_get_by_imsi(dbc, "123456789000002123456", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000002123456': No such subscriber
+
+db_subscr_create(dbc, "foobar123") --> -EINVAL
+DAUC Cannot create subscriber: invalid IMSI: 'foobar123'
+
+db_subscr_get_by_imsi(dbc, "foobar123", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='foobar123': No such subscriber
+
+db_subscr_create(dbc, "123") --> -EINVAL
+DAUC Cannot create subscriber: invalid IMSI: '123'
+
+db_subscr_get_by_imsi(dbc, "123", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123': No such subscriber
+
+db_subscr_create(dbc, short_imsi) --> 0
+
+db_subscr_get_by_imsi(dbc, short_imsi, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 4,
+  .imsi = '123456',
+}
+
+
+--- Set valid / invalid MSISDN
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "54321") --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '54321',
+}
+
+db_subscr_get_by_msisdn(dbc, "54321", &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '54321',
+}
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "54321012345678912345678") --> -EINVAL
+DAUC IMSI='123456789000000': Cannot update subscriber: invalid MSISDN: '54321012345678912345678'
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '54321',
+}
+
+db_subscr_get_by_msisdn(dbc, "54321", &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '54321',
+}
+
+db_subscr_get_by_msisdn(dbc, "54321012345678912345678", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='54321012345678912345678': No such subscriber
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "543 21") --> -EINVAL
+DAUC IMSI='123456789000000': Cannot update subscriber: invalid MSISDN: '543 21'
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '54321',
+}
+
+db_subscr_get_by_msisdn(dbc, "543 21", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='543 21': No such subscriber
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "foobar123") --> -EINVAL
+DAUC IMSI='123456789000000': Cannot update subscriber: invalid MSISDN: 'foobar123'
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '54321',
+}
+
+db_subscr_get_by_msisdn(dbc, "foobar123", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='foobar123': No such subscriber
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "5") --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '5',
+}
+
+db_subscr_get_by_msisdn(dbc, "5", &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '5',
+}
+
+db_subscr_get_by_msisdn(dbc, "54321", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='54321': No such subscriber
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "543210123456789") --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_get_by_msisdn(dbc, "543210123456789", &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_update_msisdn_by_imsi(dbc, imsi0, "5432101234567891") --> -EINVAL
+DAUC IMSI='123456789000000': Cannot update subscriber: invalid MSISDN: '5432101234567891'
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_get_by_msisdn(dbc, "5432101234567891", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='5432101234567891': No such subscriber
+
+
+--- Set MSISDN on non-existent / invalid IMSI
+
+db_subscr_update_msisdn_by_imsi(dbc, unknown_imsi, "99") --> -ENOENT
+DAUC Cannot update MSISDN: no such subscriber: IMSI='999999999'
+
+db_subscr_get_by_msisdn(dbc, "99", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='99': No such subscriber
+
+db_subscr_update_msisdn_by_imsi(dbc, "foobar", "99") --> -ENOENT
+DAUC Cannot update MSISDN: no such subscriber: IMSI='foobar'
+
+db_subscr_get_by_msisdn(dbc, "99", &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: MSISDN='99': No such subscriber
+
+
+--- Set / unset nam_cs and nam_ps
+
+db_subscr_nam(dbc, imsi0, false, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, false, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_cs = false,
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, true, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, true, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+
+--- Set / unset nam_cs and nam_ps *again*
+
+db_subscr_nam(dbc, imsi0, false, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, false, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, false, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_cs = false,
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, false, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_cs = false,
+  .nam_ps = false,
+}
+
+db_subscr_nam(dbc, imsi0, true, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_cs = false,
+}
+
+db_subscr_nam(dbc, imsi0, true, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .nam_cs = false,
+}
+
+db_subscr_nam(dbc, imsi0, true, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_nam(dbc, imsi0, true, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+
+--- Set nam_cs and nam_ps on non-existent / invalid IMSI
+
+db_subscr_nam(dbc, unknown_imsi, false, true) --> -ENOENT
+DAUC Cannot disable PS: no such subscriber: IMSI='999999999'
+
+db_subscr_nam(dbc, unknown_imsi, false, false) --> -ENOENT
+DAUC Cannot disable CS: no such subscriber: IMSI='999999999'
+
+db_subscr_get_by_imsi(dbc, unknown_imsi, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='999999999': No such subscriber
+
+db_subscr_nam(dbc, "foobar", false, true) --> -ENOENT
+DAUC Cannot disable PS: no such subscriber: IMSI='foobar'
+
+db_subscr_nam(dbc, "foobar", false, false) --> -ENOENT
+DAUC Cannot disable CS: no such subscriber: IMSI='foobar'
+
+
+--- Record LU for PS and CS (SGSN and VLR names)
+
+db_subscr_lu(dbc, id0, "5952", true) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .sgsn_number = '5952',
+}
+
+db_subscr_lu(dbc, id0, "712", false) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '712',
+  .sgsn_number = '5952',
+}
+
+
+--- Record LU for PS and CS (SGSN and VLR names) *again*
+
+db_subscr_lu(dbc, id0, "111", true) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '712',
+  .sgsn_number = '111',
+}
+
+db_subscr_lu(dbc, id0, "111", true) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '712',
+  .sgsn_number = '111',
+}
+
+db_subscr_lu(dbc, id0, "222", false) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '222',
+  .sgsn_number = '111',
+}
+
+db_subscr_lu(dbc, id0, "222", false) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '222',
+  .sgsn_number = '111',
+}
+
+
+--- Unset LU info for PS and CS (SGSN and VLR names)
+
+db_subscr_lu(dbc, id0, "", true) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '222',
+}
+
+db_subscr_lu(dbc, id0, "", false) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_lu(dbc, id0, "111", true) --> 0
+
+db_subscr_lu(dbc, id0, "222", false) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '222',
+  .sgsn_number = '111',
+}
+
+db_subscr_lu(dbc, id0, NULL, true) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .vlr_number = '222',
+}
+
+db_subscr_lu(dbc, id0, NULL, false) --> 0
+
+db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+
+--- Record LU for non-existent ID
+
+db_subscr_lu(dbc, 99999, "5952", true) --> -ENOENT
+DAUC Cannot update SGSN number for subscriber ID=99999: no such subscriber
+
+db_subscr_lu(dbc, 99999, "712", false) --> -ENOENT
+DAUC Cannot update VLR number for subscriber ID=99999: no such subscriber
+
+db_subscr_get_by_id(dbc, 99999, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: ID=99999: No such subscriber
+
+
+--- Purge and un-purge PS and CS
+
+db_subscr_purge(dbc, imsi0, true, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_ps = true,
+}
+
+db_subscr_purge(dbc, imsi0, true, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_cs = true,
+  .ms_purged_ps = true,
+}
+
+db_subscr_purge(dbc, imsi0, false, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_ps = true,
+}
+
+db_subscr_purge(dbc, imsi0, false, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+
+--- Purge PS and CS *again*
+
+db_subscr_purge(dbc, imsi0, true, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_ps = true,
+}
+
+db_subscr_purge(dbc, imsi0, true, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_ps = true,
+}
+
+db_subscr_purge(dbc, imsi0, false, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_purge(dbc, imsi0, false, true) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_purge(dbc, imsi0, true, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_cs = true,
+}
+
+db_subscr_purge(dbc, imsi0, true, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+  .ms_purged_cs = true,
+}
+
+db_subscr_purge(dbc, imsi0, false, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_purge(dbc, imsi0, false, false) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+
+--- Purge on non-existent / invalid IMSI
+
+db_subscr_purge(dbc, unknown_imsi, true, true) --> -ENOENT
+DAUC Cannot purge PS: no such subscriber: IMSI='999999999'
+
+db_subscr_get_by_imsi(dbc, unknown_imsi, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='999999999': No such subscriber
+
+db_subscr_purge(dbc, unknown_imsi, true, false) --> -ENOENT
+DAUC Cannot purge CS: no such subscriber: IMSI='999999999'
+
+db_subscr_get_by_imsi(dbc, unknown_imsi, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='999999999': No such subscriber
+
+
+--- Delete non-existent / invalid IDs
+
+db_subscr_delete_by_id(dbc, 999) --> -ENOENT
+DAUC Cannot delete: no such subscriber: ID=999
+
+db_subscr_delete_by_id(dbc, -10) --> -ENOENT
+DAUC Cannot delete: no such subscriber: ID=-10
+
+
+--- Delete subscribers
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+  .msisdn = '543210123456789',
+}
+
+db_subscr_delete_by_id(dbc, id0) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000000': No such subscriber
+
+db_subscr_delete_by_id(dbc, id0) --> -ENOENT
+DAUC Cannot delete: no such subscriber: ID=1
+
+db_subscr_get_by_imsi(dbc, imsi1, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 2,
+  .imsi = '123456789000001',
+}
+
+db_subscr_delete_by_id(dbc, id1) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi1, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000001': No such subscriber
+
+db_subscr_get_by_imsi(dbc, imsi2, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 3,
+  .imsi = '123456789000002',
+}
+
+db_subscr_delete_by_id(dbc, id2) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi2, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000002': No such subscriber
+
+db_subscr_get_by_imsi(dbc, short_imsi, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 4,
+  .imsi = '123456',
+}
+
+db_subscr_delete_by_id(dbc, id_short) --> 0
+
+db_subscr_get_by_imsi(dbc, short_imsi, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456': No such subscriber
+
+===== test_subscr_create_update_sel_delete: SUCCESS
+
+
+===== test_subscr_aud
+
+--- Get auth data for non-existent subscriber
+
+db_get_auth_data(dbc, unknown_imsi, &g_aud2g, &g_aud3g, &g_id) --> -2
+DAUC IMSI='999999999': No such subscriber
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -2
+DAUC IMSI='123456789000000': No such subscriber
+
+
+--- Create subscriber
+
+db_subscr_create(dbc, imsi0) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+--- Set auth data, 2G only
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_COMP128v1, "0123456789abcdef0123456789abcdef")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v1,
+  .u.gsm.ki = '0123456789abcdef0123456789abcdef',
+}
+3G: none
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> 3
+DAUC IMSI='123456789000000': No 3G Auth Data
+DAUC IMSI='123456789000000': Calling to generate 3 vectors
+DAUC IMSI='123456789000000': Generated 3 vectors
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_COMP128v1, "0123456789abcdef0123456789abcdef")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v1,
+  .u.gsm.ki = '0123456789abcdef0123456789abcdef',
+}
+3G: none
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_COMP128v2, "BeadedBeeAced1EbbedDefacedFacade")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v2,
+  .u.gsm.ki = 'beadedbeeaced1ebbeddefacedfacade',
+}
+3G: none
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_COMP128v3, "DeafBeddedBabeAcceededFadedDecaf")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'deafbeddedbabeacceededfadeddecaf',
+}
+3G: none
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_XOR, "CededEffacedAceFacedBadFadedBeef")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = XOR,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: none
+
+
+--- Remove 2G auth data
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_NONE, NULL)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_NONE, NULL)) --> -ENOENT
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_XOR, "CededEffacedAceFacedBadFadedBeef")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = XOR,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: none
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_NONE, "f000000000000f00000000000f000000")) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+--- Set auth data, 3G only
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "BeefedCafeFaceAcedAddedDecadeFee", true, "C01ffedC1cadaeAc1d1f1edAcac1aB0a", 5)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> 3
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': Calling to generate 3 vectors
+DAUC IMSI='123456789000000': Generated 3 vectors
+DAUC IMSI='123456789000000': Updating SQN=0 in DB
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "BeefedCafeFaceAcedAddedDecadeFee", true, "C01ffedC1cadaeAc1d1f1edAcac1aB0a", 5)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "Deaf0ff1ceD0d0DabbedD1ced1ceF00d", true, "F1bbed0afD0eF0bD0ffed0ddF1fe0b0e", 0)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'deaf0ff1ced0d0dabbedd1ced1cef00d',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'f1bbed0afd0ef0bd0ffed0ddf1fe0b0e',
+  .u.umts.amf = '0000',
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "BeefedCafeFaceAcedAddedDecadeFee", false, "DeafBeddedBabeAcceededFadedDecaf", OSMO_MILENAGE_IND_BITLEN_MAX)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 28,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "CededEffacedAceFacedBadFadedBeef", false, "BeefedCafeFaceAcedAddedDecadeFee", 5)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'cededeffacedacefacedbadfadedbeef',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+
+--- Remove 3G auth data
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_NONE, NULL, false, NULL, 0)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_NONE, NULL, false, NULL, 0)) --> -ENOENT
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "CededEffacedAceFacedBadFadedBeef", false, "BeefedCafeFaceAcedAddedDecadeFee", 5)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'cededeffacedacefacedbadfadedbeef',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> 3
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': Calling to generate 3 vectors
+DAUC IMSI='123456789000000': Generated 3 vectors
+DAUC IMSI='123456789000000': Updating SQN=0 in DB
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_NONE, "asdfasdfasd", false, "asdfasdfasdf", 99999)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+--- Set auth data, 2G and 3G
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_COMP128v3, "CededEffacedAceFacedBadFadedBeef")) --> 0
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "BeefedCafeFaceAcedAddedDecadeFee", false, "DeafBeddedBabeAcceededFadedDecaf", 5)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> 3
+DAUC IMSI='123456789000000': Calling to generate 3 vectors
+DAUC IMSI='123456789000000': Generated 3 vectors
+DAUC IMSI='123456789000000': Updating SQN=0 in DB
+
+
+--- Set invalid auth data
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(99999, "f000000000000f00000000000f000000")) --> -EINVAL
+DAUC Cannot update auth tokens: Unknown auth algo: 99999
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_XOR, "f000000000000f00000000000f000000f00000000")) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid KI: 'f000000000000f00000000000f000000f00000000'
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_XOR, "f00")) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid KI: 'f00'
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_2g(OSMO_AUTH_ALG_MILENAGE, "0123456789abcdef0123456789abcdef")) --> -EINVAL
+DAUC Cannot update auth tokens: auth algo not suited for 2G: MILENAGE
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "0f000000000000f00000000000f000000", false, "f000000000000f00000000000f000000", 5)) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid OP/OPC: '0f000000000000f00000000000f000000'
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "f000000000000f00000000000f000000", false, "000000000000f00000000000f000000", 5)) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid K: '000000000000f00000000000f000000'
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "f000000000000f00000000000f000000", false, "f000000000000f00000000000f000000", OSMO_MILENAGE_IND_BITLEN_MAX + 1)) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid ind_bitlen: 29
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "X000000000000f00000000000f000000", false, "f000000000000f00000000000f000000", 5)) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid OP/OPC: 'X000000000000f00000000000f000000'
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "f000000000000f00000000000f000000", false, "f000000000000 f00000000000 f000000", 5)) --> -EINVAL
+DAUC Cannot update auth tokens: Invalid K: 'f000000000000 f00000000000 f000000'
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+
+2G: struct osmo_sub_auth_data {
+  .type = GSM,
+  .algo = COMP128v3,
+  .u.gsm.ki = 'cededeffacedacefacedbadfadedbeef',
+}
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 0,
+  .u.umts.k = 'deafbeddedbabeacceededfadeddecaf',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+
+--- Delete subscriber
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_subscr_delete_by_id(dbc, id) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000000': No such subscriber
+
+
+--- Re-add subscriber and verify auth data didn't come back
+
+db_subscr_create(dbc, imsi0) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+db_subscr_delete_by_id(dbc, id) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000000': No such subscriber
+
+db_get_auc(dbc, imsi0, 3, vec, N_VECTORS, NULL, NULL) --> -2
+DAUC IMSI='123456789000000': No such subscriber
+
+===== test_subscr_aud: SUCCESS
+
+
+===== test_subscr_sqn
+
+--- Set SQN for unknown subscriber
+
+db_update_sqn(dbc, 99, 999) --> -ENOENT
+DAUC Cannot update SQN for subscriber ID=99: no auc_3g entry for such subscriber
+
+db_subscr_get_by_id(dbc, 99, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: ID=99: No such subscriber
+
+db_update_sqn(dbc, 9999, 99) --> -ENOENT
+DAUC Cannot update SQN for subscriber ID=9999: no auc_3g entry for such subscriber
+
+db_subscr_get_by_id(dbc, 9999, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: ID=9999: No such subscriber
+
+
+--- Create subscriber
+
+db_subscr_create(dbc, imsi0) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+
+--- Set SQN, but no 3G auth data present
+
+db_update_sqn(dbc, id, 123) --> -ENOENT
+DAUC Cannot update SQN for subscriber ID=1: no auc_3g entry for such subscriber
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+db_update_sqn(dbc, id, 543) --> -ENOENT
+DAUC Cannot update SQN for subscriber ID=1: no auc_3g entry for such subscriber
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> -126
+DAUC IMSI='123456789000000': No 2G Auth Data
+DAUC IMSI='123456789000000': No 3G Auth Data
+
+
+
+--- Set auth 3G data
+
+db_subscr_update_aud_by_id(dbc, id, mk_aud_3g(OSMO_AUTH_ALG_MILENAGE, "BeefedCafeFaceAcedAddedDecadeFee", true, "C01ffedC1cadaeAc1d1f1edAcac1aB0a", 5)) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+
+--- Set SQN
+
+db_update_sqn(dbc, id, 23315) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.sqn = 23315,
+  .u.umts.sqn = 0x5b13,
+  .u.umts.ind_bitlen = 5,
+}
+
+db_update_sqn(dbc, id, 23315) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.sqn = 23315,
+  .u.umts.sqn = 0x5b13,
+  .u.umts.ind_bitlen = 5,
+}
+
+db_update_sqn(dbc, id, 423) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.sqn = 423,
+  .u.umts.sqn = 0x1a7,
+  .u.umts.ind_bitlen = 5,
+}
+
+
+--- Set SQN: thru uint64_t range, using the int64_t SQLite bind
+
+db_update_sqn(dbc, id, 0) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.ind_bitlen = 5,
+}
+
+db_update_sqn(dbc, id, INT64_MAX) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.sqn = 9223372036854775807,
+  .u.umts.sqn = 0x7fffffffffffffff,
+  .u.umts.ind_bitlen = 5,
+}
+
+db_update_sqn(dbc, id, INT64_MIN) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.sqn = 9223372036854775808,
+  .u.umts.sqn = 0x8000000000000000,
+  .u.umts.ind_bitlen = 5,
+}
+
+db_update_sqn(dbc, id, UINT64_MAX) --> 0
+
+db_get_auth_data(dbc, imsi0, &g_aud2g, &g_aud3g, &g_id) --> 0
+DAUC IMSI='123456789000000': No 2G Auth Data
+
+2G: none
+3G: struct osmo_sub_auth_data {
+  .type = UMTS,
+  .algo = MILENAGE,
+  .u.umts.opc = 'beefedcafefaceacedaddeddecadefee',
+  .u.umts.opc_is_op = 1,
+  .u.umts.k = 'c01ffedc1cadaeac1d1f1edacac1ab0a',
+  .u.umts.amf = '0000',
+  .u.umts.sqn = 18446744073709551615,
+  .u.umts.sqn = 0xffffffffffffffff,
+  .u.umts.ind_bitlen = 5,
+}
+
+
+--- Delete subscriber
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> 0
+struct hlr_subscriber {
+  .id = 1,
+  .imsi = '123456789000000',
+}
+
+db_subscr_delete_by_id(dbc, id) --> 0
+
+db_subscr_get_by_imsi(dbc, imsi0, &g_subscr) --> -ENOENT
+DAUC Cannot read subscriber from db: IMSI='123456789000000': No such subscriber
+
+===== test_subscr_sqn: SUCCESS
+
diff --git a/tests/db/db_test.ok b/tests/db/db_test.ok
new file mode 100644
index 0000000..26cefd1
--- /dev/null
+++ b/tests/db/db_test.ok
@@ -0,0 +1,2 @@
+db_test.c
+Done
diff --git a/tests/gsup/Makefile.am b/tests/gsup/Makefile.am
new file mode 100644
index 0000000..7c33dbd
--- /dev/null
+++ b/tests/gsup/Makefile.am
@@ -0,0 +1,41 @@
+AM_CPPFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir)/src \
+	$(NULL)
+
+AM_CFLAGS = \
+	-Wall \
+	-ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(LIBOSMOABIS_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = \
+	$(NULL)
+
+EXTRA_DIST = \
+	gsup_test.ok \
+	gsup_test.err \
+	$(NULL)
+
+noinst_PROGRAMS = \
+	gsup_test \
+	$(NULL)
+
+gsup_test_SOURCES = \
+	gsup_test.c \
+	$(NULL)
+
+gsup_test_LDADD = \
+	$(top_srcdir)/src/luop.c \
+	$(top_srcdir)/src/gsup_server.c \
+	$(top_srcdir)/src/gsup_router.c \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(LIBOSMOABIS_LIBS) \
+	$(NULL)
+
+.PHONY: update_exp
+update_exp:
+	$(builddir)/gsup_test >"$(srcdir)/gsup_test.ok" 2>"$(srcdir)/gsup_test.err"
diff --git a/tests/gsup/gsup_test.c b/tests/gsup/gsup_test.c
new file mode 100644
index 0000000..41865d2
--- /dev/null
+++ b/tests/gsup/gsup_test.c
@@ -0,0 +1,91 @@
+/* (C) 2018 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * 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 <string.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/application.h>
+#include <osmocom/gsm/gsup.h>
+
+#include "logging.h"
+#include "luop.h"
+
+struct osmo_gsup_server;
+
+/* override osmo_gsup_addr_send() to not actually send anything. */
+int osmo_gsup_addr_send(struct osmo_gsup_server *gs,
+			const uint8_t *addr, size_t addrlen,
+			struct msgb *msg)
+{
+	LOGP(DMAIN, LOGL_DEBUG, "%s\n", msgb_hexdump(msg));
+	msgb_free(msg);
+	return 0;
+}
+
+int db_subscr_get_by_imsi(struct db_context *dbc, const char *imsi,
+			  struct hlr_subscriber *subscr)
+{
+	return 0;
+}
+
+/* Verify that the internally allocated msgb is large enough */
+void test_gsup_tx_insert_subscr_data()
+{
+	struct lu_operation luop = {
+		.state = LU_S_LU_RECEIVED,
+		.subscr = {
+			.imsi = "123456789012345",
+			.msisdn = "987654321098765",
+			.nam_cs = true,
+			.nam_ps = true,
+		},
+		.is_ps = true,
+	};
+
+	lu_op_tx_insert_subscr_data(&luop);
+}
+
+const struct log_info_cat default_categories[] = {
+	[DMAIN] = {
+		.name = "DMAIN",
+		.description = "Main Program",
+		.enabled = 1, .loglevel = LOGL_DEBUG,
+	},
+};
+
+static struct log_info info = {
+	.cat = default_categories,
+	.num_cat = ARRAY_SIZE(default_categories),
+};
+
+int main(int argc, char **argv)
+{
+	void *ctx = talloc_named_const(NULL, 0, "gsup_test");
+	osmo_init_logging2(ctx, &info);
+	log_set_print_filename(osmo_stderr_target, 0);
+	log_set_print_timestamp(osmo_stderr_target, 0);
+	log_set_use_color(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+
+	test_gsup_tx_insert_subscr_data();
+
+	printf("Done.\n");
+	return EXIT_SUCCESS;
+}
diff --git a/tests/gsup/gsup_test.err b/tests/gsup/gsup_test.err
new file mode 100644
index 0000000..0aeae30
--- /dev/null
+++ b/tests/gsup/gsup_test.err
@@ -0,0 +1,2 @@
+DMAIN 10 01 08 21 43 65 87 09 21 43 f5 08 09 08 89 67 45 23 01 89 67 f5 05 07 10 01 01 12 02 01 2a 28 01 01 
+DMAIN LU OP state change: LU RECEIVED -> ISD SENT
diff --git a/tests/gsup/gsup_test.ok b/tests/gsup/gsup_test.ok
new file mode 100644
index 0000000..619c561
--- /dev/null
+++ b/tests/gsup/gsup_test.ok
@@ -0,0 +1 @@
+Done.
diff --git a/tests/gsup_server/Makefile.am b/tests/gsup_server/Makefile.am
new file mode 100644
index 0000000..fee60f5
--- /dev/null
+++ b/tests/gsup_server/Makefile.am
@@ -0,0 +1,40 @@
+AM_CPPFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir)/src \
+	$(NULL)
+
+AM_CFLAGS = \
+	-Wall \
+	-ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(LIBOSMOABIS_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = \
+	$(NULL)
+
+EXTRA_DIST = \
+	gsup_server_test.ok \
+	gsup_server_test.err \
+	$(NULL)
+
+noinst_PROGRAMS = \
+	gsup_server_test \
+	$(NULL)
+
+gsup_server_test_SOURCES = \
+	gsup_server_test.c \
+	$(NULL)
+
+gsup_server_test_LDADD = \
+	$(top_srcdir)/src/gsup_server.c \
+	$(top_srcdir)/src/gsup_router.c \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(LIBOSMOABIS_LIBS) \
+	$(NULL)
+
+.PHONY: update_exp
+update_exp:
+	$(builddir)/gsup_server_test >"$(srcdir)/gsup_server_test.ok" 2>"$(srcdir)/gsup_server_test.err"
diff --git a/tests/gsup_server/gsup_server_test.c b/tests/gsup_server/gsup_server_test.c
new file mode 100644
index 0000000..cc475be
--- /dev/null
+++ b/tests/gsup_server/gsup_server_test.c
@@ -0,0 +1,145 @@
+/* (C) 2017 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 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 <osmocom/core/utils.h>
+#include "gsup_server.h"
+
+#define comment_start() printf("\n===== %s\n", __func__)
+#define comment_end() printf("===== %s: SUCCESS\n\n", __func__)
+#define btw(fmt, args...) printf("\n" fmt "\n", ## args)
+
+#define VERBOSE_ASSERT(val, expect_op, fmt) \
+	do { \
+		printf(#val " == " fmt "\n", (val)); \
+		OSMO_ASSERT((val) expect_op); \
+	} while (0)
+
+void osmo_gsup_server_add_conn(struct llist_head *clients,
+			       struct osmo_gsup_conn *conn);
+
+static void test_add_conn(void)
+{
+	struct llist_head _list;
+	struct llist_head *clients = &_list;
+	struct osmo_gsup_conn conn_inst[23] = {};
+	struct osmo_gsup_conn *conn;
+	unsigned int i;
+
+	comment_start();
+
+	INIT_LLIST_HEAD(clients);
+
+	btw("Add 10 items");
+	for (i = 0; i < 10; i++) {
+		osmo_gsup_server_add_conn(clients, &conn_inst[i]);
+		printf("conn_inst[%u].auc_3g_ind == %u\n", i, conn_inst[i].auc_3g_ind);
+		OSMO_ASSERT(clients->next == &conn_inst[0].list);
+	}
+
+	btw("Expecting a list of 0..9");
+	i = 0;
+	llist_for_each_entry(conn, clients, list) {
+		printf("conn[%u].auc_3g_ind == %u\n", i, conn->auc_3g_ind);
+		OSMO_ASSERT(conn->auc_3g_ind == i);
+		OSMO_ASSERT(conn == &conn_inst[i]);
+		i++;
+	}
+
+	btw("Punch two holes in the sequence in arbitrary order,"
+	    " a larger one from 2..4 and a single one at 7.");
+	llist_del(&conn_inst[4].list);
+	llist_del(&conn_inst[2].list);
+	llist_del(&conn_inst[3].list);
+	llist_del(&conn_inst[7].list);
+
+	btw("Expecting a list of 0,1, 5,6, 8,9");
+	i = 0;
+	llist_for_each_entry(conn, clients, list) {
+		printf("conn[%u].auc_3g_ind == %u\n", i, conn->auc_3g_ind);
+		i++;
+	}
+
+	btw("Add conns, expecting them to take the open slots");
+	osmo_gsup_server_add_conn(clients, &conn_inst[12]);
+	VERBOSE_ASSERT(conn_inst[12].auc_3g_ind, == 2, "%u");
+
+	osmo_gsup_server_add_conn(clients, &conn_inst[13]);
+	VERBOSE_ASSERT(conn_inst[13].auc_3g_ind, == 3, "%u");
+
+	osmo_gsup_server_add_conn(clients, &conn_inst[14]);
+	VERBOSE_ASSERT(conn_inst[14].auc_3g_ind, == 4, "%u");
+
+	osmo_gsup_server_add_conn(clients, &conn_inst[17]);
+	VERBOSE_ASSERT(conn_inst[17].auc_3g_ind, == 7, "%u");
+
+	osmo_gsup_server_add_conn(clients, &conn_inst[18]);
+	VERBOSE_ASSERT(conn_inst[18].auc_3g_ind, == 10, "%u");
+
+	btw("Expecting a list of 0..10");
+	i = 0;
+	llist_for_each_entry(conn, clients, list) {
+		printf("conn[%u].auc_3g_ind == %u\n", i, conn->auc_3g_ind);
+		OSMO_ASSERT(conn->auc_3g_ind == i);
+		i++;
+	}
+
+	btw("Does it also work for the first item?");
+	llist_del(&conn_inst[0].list);
+
+	btw("Expecting a list of 1..10");
+	i = 0;
+	llist_for_each_entry(conn, clients, list) {
+		printf("conn[%u].auc_3g_ind == %u\n", i, conn->auc_3g_ind);
+		OSMO_ASSERT(conn->auc_3g_ind == i + 1);
+		i++;
+	}
+
+	btw("Add another conn, should take auc_3g_ind == 0");
+	osmo_gsup_server_add_conn(clients, &conn_inst[20]);
+	VERBOSE_ASSERT(conn_inst[20].auc_3g_ind, == 0, "%u");
+
+	btw("Expecting a list of 0..10");
+	i = 0;
+	llist_for_each_entry(conn, clients, list) {
+		printf("conn[%u].auc_3g_ind == %u\n", i, conn->auc_3g_ind);
+		OSMO_ASSERT(conn->auc_3g_ind == i);
+		i++;
+	}
+
+	btw("If a client reconnects, it will (likely) get the same auc_3g_ind");
+	VERBOSE_ASSERT(conn_inst[5].auc_3g_ind, == 5, "%u");
+	llist_del(&conn_inst[5].list);
+	conn_inst[5].auc_3g_ind = 423;
+	osmo_gsup_server_add_conn(clients, &conn_inst[5]);
+	VERBOSE_ASSERT(conn_inst[5].auc_3g_ind, == 5, "%u");
+
+	comment_end();
+}
+
+int main(int argc, char **argv)
+{
+	printf("test_gsup_server.c\n");
+
+	test_add_conn();
+
+	printf("Done\n");
+	return 0;
+}
diff --git a/tests/gsup_server/gsup_server_test.err b/tests/gsup_server/gsup_server_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/gsup_server/gsup_server_test.err
diff --git a/tests/gsup_server/gsup_server_test.ok b/tests/gsup_server/gsup_server_test.ok
new file mode 100644
index 0000000..80d944c
--- /dev/null
+++ b/tests/gsup_server/gsup_server_test.ok
@@ -0,0 +1,94 @@
+test_gsup_server.c
+
+===== test_add_conn
+
+Add 10 items
+conn_inst[0].auc_3g_ind == 0
+conn_inst[1].auc_3g_ind == 1
+conn_inst[2].auc_3g_ind == 2
+conn_inst[3].auc_3g_ind == 3
+conn_inst[4].auc_3g_ind == 4
+conn_inst[5].auc_3g_ind == 5
+conn_inst[6].auc_3g_ind == 6
+conn_inst[7].auc_3g_ind == 7
+conn_inst[8].auc_3g_ind == 8
+conn_inst[9].auc_3g_ind == 9
+
+Expecting a list of 0..9
+conn[0].auc_3g_ind == 0
+conn[1].auc_3g_ind == 1
+conn[2].auc_3g_ind == 2
+conn[3].auc_3g_ind == 3
+conn[4].auc_3g_ind == 4
+conn[5].auc_3g_ind == 5
+conn[6].auc_3g_ind == 6
+conn[7].auc_3g_ind == 7
+conn[8].auc_3g_ind == 8
+conn[9].auc_3g_ind == 9
+
+Punch two holes in the sequence in arbitrary order, a larger one from 2..4 and a single one at 7.
+
+Expecting a list of 0,1, 5,6, 8,9
+conn[0].auc_3g_ind == 0
+conn[1].auc_3g_ind == 1
+conn[2].auc_3g_ind == 5
+conn[3].auc_3g_ind == 6
+conn[4].auc_3g_ind == 8
+conn[5].auc_3g_ind == 9
+
+Add conns, expecting them to take the open slots
+conn_inst[12].auc_3g_ind == 2
+conn_inst[13].auc_3g_ind == 3
+conn_inst[14].auc_3g_ind == 4
+conn_inst[17].auc_3g_ind == 7
+conn_inst[18].auc_3g_ind == 10
+
+Expecting a list of 0..10
+conn[0].auc_3g_ind == 0
+conn[1].auc_3g_ind == 1
+conn[2].auc_3g_ind == 2
+conn[3].auc_3g_ind == 3
+conn[4].auc_3g_ind == 4
+conn[5].auc_3g_ind == 5
+conn[6].auc_3g_ind == 6
+conn[7].auc_3g_ind == 7
+conn[8].auc_3g_ind == 8
+conn[9].auc_3g_ind == 9
+conn[10].auc_3g_ind == 10
+
+Does it also work for the first item?
+
+Expecting a list of 1..10
+conn[0].auc_3g_ind == 1
+conn[1].auc_3g_ind == 2
+conn[2].auc_3g_ind == 3
+conn[3].auc_3g_ind == 4
+conn[4].auc_3g_ind == 5
+conn[5].auc_3g_ind == 6
+conn[6].auc_3g_ind == 7
+conn[7].auc_3g_ind == 8
+conn[8].auc_3g_ind == 9
+conn[9].auc_3g_ind == 10
+
+Add another conn, should take auc_3g_ind == 0
+conn_inst[20].auc_3g_ind == 0
+
+Expecting a list of 0..10
+conn[0].auc_3g_ind == 0
+conn[1].auc_3g_ind == 1
+conn[2].auc_3g_ind == 2
+conn[3].auc_3g_ind == 3
+conn[4].auc_3g_ind == 4
+conn[5].auc_3g_ind == 5
+conn[6].auc_3g_ind == 6
+conn[7].auc_3g_ind == 7
+conn[8].auc_3g_ind == 8
+conn[9].auc_3g_ind == 9
+conn[10].auc_3g_ind == 10
+
+If a client reconnects, it will (likely) get the same auc_3g_ind
+conn_inst[5].auc_3g_ind == 5
+conn_inst[5].auc_3g_ind == 5
+===== test_add_conn: SUCCESS
+
+Done
diff --git a/tests/test_nodes.vty b/tests/test_nodes.vty
new file mode 100644
index 0000000..a9d4ac4
--- /dev/null
+++ b/tests/test_nodes.vty
@@ -0,0 +1,121 @@
+OsmoHLR> list
+  show version
+  show online-help
+  list
+  exit
+  help
+  enable
+  terminal length <0-512>
+  terminal no length
+  who
+  show history
+  logging enable
+...
+  show logging vty
+  show alarms
+  show talloc-context (application|all) (full|brief|DEPTH)
+  show talloc-context (application|all) (full|brief|DEPTH) tree ADDRESS
+  show talloc-context (application|all) (full|brief|DEPTH) filter REGEXP
+  show gsup-connections
+  subscriber (imsi|msisdn|id) IDENT show
+
+OsmoHLR> enable
+OsmoHLR# list
+  help
+  list
+  write terminal
+  write file
+  write memory
+  write
+  show running-config
+  exit
+  disable
+  configure terminal
+  copy running-config startup-config
+  show startup-config
+  show version
+  show online-help
+  terminal length <0-512>
+  terminal no length
+  who
+  show history
+  terminal monitor
+  terminal no monitor
+  logging enable
+...
+
+OsmoHLR# configure terminal
+OsmoHLR(config)# list
+  help
+  list
+  write terminal
+  write file
+  write memory
+  write
+  show running-config
+  exit
+  end
+...
+  hlr
+
+OsmoHLR(config)# hlr
+OsmoHLR(config-hlr)# list
+  help
+  list
+  write terminal
+  write file
+  write memory
+  write
+  show running-config
+  exit
+  end
+  gsup
+  euse NAME
+  no euse NAME
+  ussd route prefix PREFIX internal (own-msisdn|own-imsi)
+  ussd route prefix PREFIX external EUSE
+  no ussd route prefix PREFIX
+  ussd default-route external EUSE
+  no ussd default-route
+
+OsmoHLR(config-hlr)# gsup
+OsmoHLR(config-hlr-gsup)# list
+  help
+  list
+  write terminal
+  write file
+  write memory
+  write
+  show running-config
+  exit
+  end
+  bind ip A.B.C.D
+
+OsmoHLR(config-hlr-gsup)# exit
+OsmoHLR(config-hlr)# exit
+OsmoHLR(config)# exit
+OsmoHLR# configure terminal
+OsmoHLR(config)# hlr
+OsmoHLR(config-hlr)# gsup
+OsmoHLR(config-hlr-gsup)# end
+OsmoHLR# disable
+OsmoHLR> enable
+
+OsmoHLR# show running-config
+
+Current configuration:
+!
+!
+log stderr
+...
+ logging level main notice
+ logging level db notice
+ logging level auc notice
+ logging level ss info
+...
+hlr
+ gsup
+  bind ip 127.0.0.1
+ ussd route prefix *#100# internal own-msisdn
+ ussd route prefix *#101# internal own-imsi
+end
diff --git a/tests/test_subscriber.ctrl b/tests/test_subscriber.ctrl
new file mode 100644
index 0000000..4cefa4d
--- /dev/null
+++ b/tests/test_subscriber.ctrl
@@ -0,0 +1,614 @@
+GET 1 subscriber.by-imsi-901990000000001.info
+GET_REPLY 1 subscriber.by-imsi-901990000000001.info 
+id	1
+imsi	901990000000001
+msisdn	1
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 2 subscriber.by-imsi-901990000000001.info-aud
+GET_REPLY 2 subscriber.by-imsi-901990000000001.info-aud 
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+
+GET 3 subscriber.by-imsi-901990000000001.info-all
+GET_REPLY 3 subscriber.by-imsi-901990000000001.info-all 
+id	1
+imsi	901990000000001
+msisdn	1
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+
+GET 4 subscriber.by-imsi-901990000000002.info
+GET_REPLY 4 subscriber.by-imsi-901990000000002.info 
+id	2
+imsi	901990000000002
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 5 subscriber.by-imsi-901990000000002.info-aud
+GET_REPLY 5 subscriber.by-imsi-901990000000002.info-aud 
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	4223
+
+GET 6 subscriber.by-imsi-901990000000002.info-all
+GET_REPLY 6 subscriber.by-imsi-901990000000002.info-all 
+id	2
+imsi	901990000000002
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	4223
+
+GET 7 subscriber.by-imsi-901990000000003.info
+GET_REPLY 7 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 8 subscriber.by-imsi-901990000000003.info-aud
+GET_REPLY 8 subscriber.by-imsi-901990000000003.info-aud 
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	2342
+
+GET 9 subscriber.by-imsi-901990000000003.info-all
+GET_REPLY 9 subscriber.by-imsi-901990000000003.info-all 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	2342
+
+GET 10 subscriber.by-imsi-901990000000003.ps-enabled
+GET_REPLY 10 subscriber.by-imsi-901990000000003.ps-enabled 1
+
+SET 11 subscriber.by-imsi-901990000000003.ps-enabled 0
+SET_REPLY 11 subscriber.by-imsi-901990000000003.ps-enabled OK
+GET 12 subscriber.by-imsi-901990000000003.ps-enabled
+GET_REPLY 12 subscriber.by-imsi-901990000000003.ps-enabled 0
+
+GET 13 subscriber.by-imsi-901990000000003.info
+GET_REPLY 13 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	0
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 14 subscriber.by-imsi-901990000000003.ps-enabled 0
+SET_REPLY 14 subscriber.by-imsi-901990000000003.ps-enabled OK
+GET 15 subscriber.by-imsi-901990000000003.ps-enabled
+GET_REPLY 15 subscriber.by-imsi-901990000000003.ps-enabled 0
+
+SET 16 subscriber.by-imsi-901990000000003.ps-enabled 1
+SET_REPLY 16 subscriber.by-imsi-901990000000003.ps-enabled OK
+GET 17 subscriber.by-imsi-901990000000003.ps-enabled
+GET_REPLY 17 subscriber.by-imsi-901990000000003.ps-enabled 1
+
+GET 18 subscriber.by-imsi-901990000000003.info
+GET_REPLY 18 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 19 subscriber.by-imsi-901990000000003.ps-enabled 1
+SET_REPLY 19 subscriber.by-imsi-901990000000003.ps-enabled OK
+GET 20 subscriber.by-imsi-901990000000003.ps-enabled
+GET_REPLY 20 subscriber.by-imsi-901990000000003.ps-enabled 1
+
+GET 21 subscriber.by-imsi-901990000000003.cs-enabled
+GET_REPLY 21 subscriber.by-imsi-901990000000003.cs-enabled 1
+
+SET 22 subscriber.by-imsi-901990000000003.cs-enabled 0
+SET_REPLY 22 subscriber.by-imsi-901990000000003.cs-enabled OK
+GET 23 subscriber.by-imsi-901990000000003.cs-enabled
+GET_REPLY 23 subscriber.by-imsi-901990000000003.cs-enabled 0
+
+GET 24 subscriber.by-imsi-901990000000003.info
+GET_REPLY 24 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	0
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 25 subscriber.by-imsi-901990000000003.cs-enabled 0
+SET_REPLY 25 subscriber.by-imsi-901990000000003.cs-enabled OK
+GET 26 subscriber.by-imsi-901990000000003.cs-enabled
+GET_REPLY 26 subscriber.by-imsi-901990000000003.cs-enabled 0
+
+SET 27 subscriber.by-imsi-901990000000003.cs-enabled 1
+SET_REPLY 27 subscriber.by-imsi-901990000000003.cs-enabled OK
+GET 28 subscriber.by-imsi-901990000000003.cs-enabled
+GET_REPLY 28 subscriber.by-imsi-901990000000003.cs-enabled 1
+
+GET 29 subscriber.by-imsi-901990000000003.info
+GET_REPLY 29 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 30 subscriber.by-imsi-901990000000003.cs-enabled 1
+SET_REPLY 30 subscriber.by-imsi-901990000000003.cs-enabled OK
+GET 31 subscriber.by-imsi-901990000000003.cs-enabled
+GET_REPLY 31 subscriber.by-imsi-901990000000003.cs-enabled 1
+
+SET 32 subscriber.by-imsi-901990000000003.ps-enabled 0
+SET_REPLY 32 subscriber.by-imsi-901990000000003.ps-enabled OK
+SET 33 subscriber.by-imsi-901990000000003.cs-enabled 0
+SET_REPLY 33 subscriber.by-imsi-901990000000003.cs-enabled OK
+GET 34 subscriber.by-imsi-901990000000003.info
+GET_REPLY 34 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	0
+nam_ps	0
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 35 subscriber.by-imsi-901990000000003.ps-enabled 1
+SET_REPLY 35 subscriber.by-imsi-901990000000003.ps-enabled OK
+SET 36 subscriber.by-imsi-901990000000003.cs-enabled 1
+SET_REPLY 36 subscriber.by-imsi-901990000000003.cs-enabled OK
+GET 37 subscriber.by-imsi-901990000000003.info
+GET_REPLY 37 subscriber.by-imsi-901990000000003.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+
+
+GET 38 subscriber.by-msisdn-103.info
+GET_REPLY 38 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 39 subscriber.by-msisdn-103.info-aud
+GET_REPLY 39 subscriber.by-msisdn-103.info-aud 
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	2342
+
+GET 40 subscriber.by-msisdn-103.info-all
+GET_REPLY 40 subscriber.by-msisdn-103.info-all 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	2342
+
+GET 41 subscriber.by-msisdn-103.ps-enabled
+GET_REPLY 41 subscriber.by-msisdn-103.ps-enabled 1
+
+SET 42 subscriber.by-msisdn-103.ps-enabled 0
+SET_REPLY 42 subscriber.by-msisdn-103.ps-enabled OK
+GET 43 subscriber.by-msisdn-103.ps-enabled
+GET_REPLY 43 subscriber.by-msisdn-103.ps-enabled 0
+
+GET 44 subscriber.by-msisdn-103.info
+GET_REPLY 44 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	0
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 45 subscriber.by-msisdn-103.ps-enabled 0
+SET_REPLY 45 subscriber.by-msisdn-103.ps-enabled OK
+GET 46 subscriber.by-msisdn-103.ps-enabled
+GET_REPLY 46 subscriber.by-msisdn-103.ps-enabled 0
+
+SET 47 subscriber.by-msisdn-103.ps-enabled 1
+SET_REPLY 47 subscriber.by-msisdn-103.ps-enabled OK
+GET 48 subscriber.by-msisdn-103.ps-enabled
+GET_REPLY 48 subscriber.by-msisdn-103.ps-enabled 1
+
+GET 49 subscriber.by-msisdn-103.info
+GET_REPLY 49 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 50 subscriber.by-msisdn-103.ps-enabled 1
+SET_REPLY 50 subscriber.by-msisdn-103.ps-enabled OK
+GET 51 subscriber.by-msisdn-103.ps-enabled
+GET_REPLY 51 subscriber.by-msisdn-103.ps-enabled 1
+
+GET 52 subscriber.by-msisdn-103.cs-enabled
+GET_REPLY 52 subscriber.by-msisdn-103.cs-enabled 1
+
+SET 53 subscriber.by-msisdn-103.cs-enabled 0
+SET_REPLY 53 subscriber.by-msisdn-103.cs-enabled OK
+GET 54 subscriber.by-msisdn-103.cs-enabled
+GET_REPLY 54 subscriber.by-msisdn-103.cs-enabled 0
+
+GET 55 subscriber.by-msisdn-103.info
+GET_REPLY 55 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	0
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 56 subscriber.by-msisdn-103.cs-enabled 0
+SET_REPLY 56 subscriber.by-msisdn-103.cs-enabled OK
+GET 57 subscriber.by-msisdn-103.cs-enabled
+GET_REPLY 57 subscriber.by-msisdn-103.cs-enabled 0
+
+SET 58 subscriber.by-msisdn-103.cs-enabled 1
+SET_REPLY 58 subscriber.by-msisdn-103.cs-enabled OK
+GET 59 subscriber.by-msisdn-103.cs-enabled
+GET_REPLY 59 subscriber.by-msisdn-103.cs-enabled 1
+
+GET 60 subscriber.by-msisdn-103.info
+GET_REPLY 60 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 61 subscriber.by-msisdn-103.cs-enabled 1
+SET_REPLY 61 subscriber.by-msisdn-103.cs-enabled OK
+GET 62 subscriber.by-msisdn-103.cs-enabled
+GET_REPLY 62 subscriber.by-msisdn-103.cs-enabled 1
+
+SET 63 subscriber.by-msisdn-103.ps-enabled 0
+SET_REPLY 63 subscriber.by-msisdn-103.ps-enabled OK
+SET 64 subscriber.by-msisdn-103.cs-enabled 0
+SET_REPLY 64 subscriber.by-msisdn-103.cs-enabled OK
+GET 65 subscriber.by-msisdn-103.info
+GET_REPLY 65 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	0
+nam_ps	0
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 66 subscriber.by-msisdn-103.ps-enabled 1
+SET_REPLY 66 subscriber.by-msisdn-103.ps-enabled OK
+SET 67 subscriber.by-msisdn-103.cs-enabled 1
+SET_REPLY 67 subscriber.by-msisdn-103.cs-enabled OK
+GET 68 subscriber.by-msisdn-103.info
+GET_REPLY 68 subscriber.by-msisdn-103.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+
+
+GET 69 subscriber.by-id-3.info
+GET_REPLY 69 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 70 subscriber.by-id-3.info-aud
+GET_REPLY 70 subscriber.by-id-3.info-aud 
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	2342
+
+GET 71 subscriber.by-id-3.info-all
+GET_REPLY 71 subscriber.by-id-3.info-all 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+aud2g.algo	COMP128v1
+aud2g.ki	000102030405060708090a0b0c0d0e0f
+aud3g.algo	MILENAGE
+aud3g.k	000102030405060708090a0b0c0d0e0f
+aud3g.opc	101112131415161718191a1b1c1d1e1f
+aud3g.ind_bitlen	5
+aud3g.sqn	2342
+
+GET 72 subscriber.by-id-3.ps-enabled
+GET_REPLY 72 subscriber.by-id-3.ps-enabled 1
+
+SET 73 subscriber.by-id-3.ps-enabled 0
+SET_REPLY 73 subscriber.by-id-3.ps-enabled OK
+GET 74 subscriber.by-id-3.ps-enabled
+GET_REPLY 74 subscriber.by-id-3.ps-enabled 0
+
+GET 75 subscriber.by-id-3.info
+GET_REPLY 75 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	0
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 76 subscriber.by-id-3.ps-enabled 0
+SET_REPLY 76 subscriber.by-id-3.ps-enabled OK
+GET 77 subscriber.by-id-3.ps-enabled
+GET_REPLY 77 subscriber.by-id-3.ps-enabled 0
+
+SET 78 subscriber.by-id-3.ps-enabled 1
+SET_REPLY 78 subscriber.by-id-3.ps-enabled OK
+GET 79 subscriber.by-id-3.ps-enabled
+GET_REPLY 79 subscriber.by-id-3.ps-enabled 1
+
+GET 80 subscriber.by-id-3.info
+GET_REPLY 80 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 81 subscriber.by-id-3.ps-enabled 1
+SET_REPLY 81 subscriber.by-id-3.ps-enabled OK
+GET 82 subscriber.by-id-3.ps-enabled
+GET_REPLY 82 subscriber.by-id-3.ps-enabled 1
+
+GET 83 subscriber.by-id-3.cs-enabled
+GET_REPLY 83 subscriber.by-id-3.cs-enabled 1
+
+SET 84 subscriber.by-id-3.cs-enabled 0
+SET_REPLY 84 subscriber.by-id-3.cs-enabled OK
+GET 85 subscriber.by-id-3.cs-enabled
+GET_REPLY 85 subscriber.by-id-3.cs-enabled 0
+
+GET 86 subscriber.by-id-3.info
+GET_REPLY 86 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	0
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 87 subscriber.by-id-3.cs-enabled 0
+SET_REPLY 87 subscriber.by-id-3.cs-enabled OK
+GET 88 subscriber.by-id-3.cs-enabled
+GET_REPLY 88 subscriber.by-id-3.cs-enabled 0
+
+SET 89 subscriber.by-id-3.cs-enabled 1
+SET_REPLY 89 subscriber.by-id-3.cs-enabled OK
+GET 90 subscriber.by-id-3.cs-enabled
+GET_REPLY 90 subscriber.by-id-3.cs-enabled 1
+
+GET 91 subscriber.by-id-3.info
+GET_REPLY 91 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 92 subscriber.by-id-3.cs-enabled 1
+SET_REPLY 92 subscriber.by-id-3.cs-enabled OK
+GET 93 subscriber.by-id-3.cs-enabled
+GET_REPLY 93 subscriber.by-id-3.cs-enabled 1
+
+SET 94 subscriber.by-id-3.ps-enabled 0
+SET_REPLY 94 subscriber.by-id-3.ps-enabled OK
+SET 95 subscriber.by-id-3.cs-enabled 0
+SET_REPLY 95 subscriber.by-id-3.cs-enabled OK
+GET 96 subscriber.by-id-3.info
+GET_REPLY 96 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	0
+nam_ps	0
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+SET 97 subscriber.by-id-3.ps-enabled 1
+SET_REPLY 97 subscriber.by-id-3.ps-enabled OK
+SET 98 subscriber.by-id-3.cs-enabled 1
+SET_REPLY 98 subscriber.by-id-3.cs-enabled OK
+GET 99 subscriber.by-id-3.info
+GET_REPLY 99 subscriber.by-id-3.info 
+id	3
+imsi	901990000000003
+msisdn	103
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 100 subscriber.by-id-00123.info
+GET_REPLY 100 subscriber.by-id-00123.info 
+id	123
+imsi	123123
+msisdn	123
+nam_cs	1
+nam_ps	1
+ms_purged_cs	0
+ms_purged_ps	0
+periodic_lu_timer	0
+periodic_rau_tau_timer	0
+lmsi	00000000
+
+GET 101 subscriber.by-id-0x0123.info
+ERROR 101 Invalid value part of 'by-xxx-value' selector.
diff --git a/tests/test_subscriber.sql b/tests/test_subscriber.sql
new file mode 100644
index 0000000..2b6afac
--- /dev/null
+++ b/tests/test_subscriber.sql
@@ -0,0 +1,17 @@
+
+-- 2G only subscriber
+INSERT INTO subscriber (id, imsi, msisdn) VALUES (1, '901990000000001', '1');
+INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki) VALUES (1, 1, '000102030405060708090a0b0c0d0e0f');
+
+-- 3G only subscriber
+INSERT INTO subscriber (id, imsi) VALUES (2, '901990000000002');
+INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, opc, sqn) VALUES (2, 5, '000102030405060708090a0b0c0d0e0f', '101112131415161718191a1b1c1d1e1f', 4223);
+
+-- 2G + 3G subscriber
+INSERT INTO subscriber (id, imsi, msisdn) VALUES (3, '901990000000003', '103');
+INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki) VALUES (3, 1, '000102030405060708090a0b0c0d0e0f');
+INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, opc, sqn) VALUES (3, 5, '000102030405060708090a0b0c0d0e0f', '101112131415161718191a1b1c1d1e1f', 2342);
+
+-- A subscriber id > 7 and > 15 to check against octal and hex notations
+INSERT INTO subscriber (id, imsi, msisdn) VALUES (123, '123123', '123');
+INSERT INTO auc_2g (subscriber_id, algo_id_2g, ki) VALUES (123, 3, 'BeefedCafeFaceAcedAddedDecadeFee');
diff --git a/tests/test_subscriber.vty b/tests/test_subscriber.vty
new file mode 100644
index 0000000..2da455f
--- /dev/null
+++ b/tests/test_subscriber.vty
@@ -0,0 +1,349 @@
+OsmoHLR> enable
+
+OsmoHLR# list
+...
+  subscriber (imsi|msisdn|id) IDENT show
+  subscriber imsi IDENT create
+  subscriber (imsi|msisdn|id) IDENT delete
+  subscriber (imsi|msisdn|id) IDENT update msisdn MSISDN
+  subscriber (imsi|msisdn|id) IDENT update aud2g none
+  subscriber (imsi|msisdn|id) IDENT update aud2g (comp128v1|comp128v2|comp128v3|xor) ki KI
+  subscriber (imsi|msisdn|id) IDENT update aud3g none
+  subscriber (imsi|msisdn|id) IDENT update aud3g milenage k K (op|opc) OP_C [ind-bitlen] [<0-28>]
+
+OsmoHLR# subscriber?
+  subscriber  Subscriber management commands
+
+OsmoHLR# subscriber ?
+  imsi    Identify subscriber by IMSI
+  msisdn  Identify subscriber by MSISDN (phone number)
+  id      Identify subscriber by database ID
+
+OsmoHLR# subscriber imsi ?
+  IDENT  IMSI/MSISDN/ID of the subscriber
+OsmoHLR# subscriber msisdn ?
+  IDENT  IMSI/MSISDN/ID of the subscriber
+OsmoHLR# subscriber id ?
+  IDENT  IMSI/MSISDN/ID of the subscriber
+
+OsmoHLR# subscriber imsi 123456789023000 show
+% No subscriber for imsi = '123456789023000'
+OsmoHLR# subscriber id 1 show
+% No subscriber for id = '1'
+OsmoHLR# subscriber msisdn 12345 show
+% No subscriber for msisdn = '12345'
+
+OsmoHLR# subscriber imsi 1234567890230001 create
+% Not a valid IMSI: 1234567890230001
+OsmoHLR# subscriber imsi 12345678902300x create
+% Not a valid IMSI: 12345678902300x
+OsmoHLR# subscriber imsi 12345 create
+% Not a valid IMSI: 12345
+
+OsmoHLR# subscriber imsi 123456789023000 create
+% Created subscriber 123456789023000
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: none
+
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: none
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: none
+OsmoHLR# subscriber msisdn 12345 show
+% No subscriber for msisdn = '12345'
+
+OsmoHLR# subscriber imsi 123456789023000 update msisdn 12345
+% Updated subscriber IMSI='123456789023000' to MSISDN='12345'
+
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 12345
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 12345
+OsmoHLR# subscriber msisdn 12345 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 12345
+
+OsmoHLR# subscriber msisdn 12345 update msisdn 423
+% Updated subscriber IMSI='123456789023000' to MSISDN='423'
+OsmoHLR# subscriber msisdn 12345 show
+% No subscriber for msisdn = '12345'
+
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+OsmoHLR# subscriber msisdn 423 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+
+OsmoHLR# subscriber imsi 123456789023000 update ?
+  msisdn  Set MSISDN (phone number) of the subscriber
+  aud2g   Set 2G authentication data
+  aud3g   Set UMTS authentication data (3G, and 2G with UMTS AKA)
+
+OsmoHLR# subscriber imsi 123456789023000 update aud2g ?
+  none       Delete 2G authentication data
+  comp128v1  Use COMP128v1 algorithm
+  comp128v2  Use COMP128v2 algorithm
+  comp128v3  Use COMP128v3 algorithm
+  xor        Use XOR algorithm
+
+OsmoHLR# subscriber imsi 123456789023000 update aud2g comp128v1 ?
+  ki  Set Ki Encryption Key
+
+OsmoHLR# subscriber imsi 123456789023000 update aud2g comp128v1 ki ?
+  KI  Ki as 32 hexadecimal characters
+
+OsmoHLR# subscriber imsi 123456789023000 update aud2g comp128v1 ki val ?
+  <cr>  
+
+OsmoHLR# subscriber imsi 123456789023000 update aud2g xor ki Deaf0ff1ceD0d0DabbedD1ced1ceF00d
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: XOR
+             KI=deaf0ff1ced0d0dabbedd1ced1cef00d
+
+OsmoHLR# subscriber imsi 123456789023000 update aud2g comp128v1 ki BeefedCafeFaceAcedAddedDecadeFee
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v1
+             KI=beefedcafefaceacedaddeddecadefee
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v1
+             KI=beefedcafefaceacedaddeddecadefee
+OsmoHLR# subscriber msisdn 423 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v1
+             KI=beefedcafefaceacedaddeddecadefee
+
+OsmoHLR# subscriber id 1 update aud2g comp128v2 ki CededEffacedAceFacedBadFadedBeef
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v2
+             KI=cededeffacedacefacedbadfadedbeef
+OsmoHLR# subscriber msisdn 423 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v2
+             KI=cededeffacedacefacedbadfadedbeef
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v2
+             KI=cededeffacedacefacedbadfadedbeef
+
+OsmoHLR# subscriber msisdn 423 update aud2g comp128v3 ki C01ffedC1cadaeAc1d1f1edAcac1aB0a
+OsmoHLR# subscriber msisdn 423 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+
+OsmoHLR# subscriber id 1 update aud2g nonsense ki BeefedCafeFaceAcedAddedDecadeFee
+% Unknown command.
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+
+OsmoHLR# subscriber id 1 update aud2g milenage ki BeefedCafeFaceAcedAddedDecadeFee
+% Unknown command.
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+
+OsmoHLR# subscriber id 1 update aud2g xor ki CoiffedCicadaeAcidifiedAcaciaBoa
+% Invalid value for KI: 'CoiffedCicadaeAcidifiedAcaciaBoa'
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+
+OsmoHLR# subscriber id 1 update aud2g xor ki C01ffedC1cadaeAc1d1f1edAcac1aB0aX
+% Invalid value for KI: 'C01ffedC1cadaeAc1d1f1edAcac1aB0aX'
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v3
+             KI=c01ffedc1cadaeac1d1f1edacac1ab0a
+
+OsmoHLR# subscriber id 1 update aud2g none
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g ?
+  none      Delete 3G authentication data
+  milenage  Use Milenage algorithm
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage ?
+  k  Set Encryption Key K
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k ?
+  K  K as 32 hexadecimal characters
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d ?
+  op   Set OP key
+  opc  Set OPC key
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d opc ?
+  OP_C  OP or OPC as 32 hexadecimal characters
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d opc CededEffacedAceFacedBadFadedBeef ?
+  [ind-bitlen]  Set IND bit length
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d opc CededEffacedAceFacedBadFadedBeef ind-bitlen ?
+  [<0-28>]  IND bit length value (default: 5)
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d opc CededEffacedAceFacedBadFadedBeef
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OPC=cededeffacedacefacedbadfadedbeef
+             IND-bitlen=5
+
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d op DeafBeddedBabeAcceededFadedDecaf
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OP=deafbeddedbabeacceededfadeddecaf
+             IND-bitlen=5
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g none
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d opc CededEffacedAceFacedBadFadedBeef ind-bitlen 23
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OPC=cededeffacedacefacedbadfadedbeef
+             IND-bitlen=23
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k CoiffedCicadaeAcidifiedAcaciaBoa opc CededEffacedAceFacedBadFadedBeef
+% Invalid value for K: 'CoiffedCicadaeAcidifiedAcaciaBoa'
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OPC=cededeffacedacefacedbadfadedbeef
+             IND-bitlen=23
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d opc CoiffedCicadaeAcidifiedAcaciaBoa
+% Invalid value for OPC: 'CoiffedCicadaeAcidifiedAcaciaBoa'
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OPC=cededeffacedacefacedbadfadedbeef
+             IND-bitlen=23
+
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d op C01ffedC1cadaeAc1d1f1edAcac1aB0a
+OsmoHLR# subscriber imsi 123456789023000 update aud3g milenage k Deaf0ff1ceD0d0DabbedD1ced1ceF00d op CoiffedCicadaeAcidifiedAcaciaBoa
+% Invalid value for OP: 'CoiffedCicadaeAcidifiedAcaciaBoa'
+OsmoHLR# subscriber imsi 123456789023000 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OP=c01ffedc1cadaeac1d1f1edacac1ab0a
+             IND-bitlen=5
+
+OsmoHLR# subscriber id 1 update aud2g comp128v2 ki CededEffacedAceFacedBadFadedBeef
+OsmoHLR# subscriber id 1 show
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: 423
+    2G auth: COMP128v2
+             KI=cededeffacedacefacedbadfadedbeef
+    3G auth: MILENAGE
+             K=deaf0ff1ced0d0dabbedd1ced1cef00d
+             OP=c01ffedc1cadaeac1d1f1edacac1ab0a
+             IND-bitlen=5
+
+OsmoHLR# subscriber imsi 123456789023000 delete
+% Deleted subscriber for IMSI '123456789023000'
+
+OsmoHLR# subscriber imsi 123456789023000 show
+% No subscriber for imsi = '123456789023000'
+OsmoHLR# subscriber id 1 show
+% No subscriber for id = '1'
+OsmoHLR# subscriber msisdn 423 show
+% No subscriber for msisdn = '423'
+
+OsmoHLR# subscriber imsi 123456789023000 create
+% Created subscriber 123456789023000
+    ID: 1
+    IMSI: 123456789023000
+    MSISDN: none
+
+OsmoHLR# subscriber imsi 123456789023000 delete
+% Deleted subscriber for IMSI '123456789023000'
diff --git a/tests/test_subscriber_errors.ctrl b/tests/test_subscriber_errors.ctrl
new file mode 100644
index 0000000..991d3df
--- /dev/null
+++ b/tests/test_subscriber_errors.ctrl
@@ -0,0 +1,107 @@
+GET 1 invalid
+ERROR 1 Command not found
+SET 2 invalid nonsense
+ERROR 2 Command not found
+
+GET 3 subscriber.by-imsi-nonsense.info
+ERROR 3 Invalid value part of 'by-xxx-value' selector.
+GET 4 subscriber.by-msisdn-nonsense.info
+ERROR 4 Invalid value part of 'by-xxx-value' selector.
+GET 5 subscriber.by-id-nonsense.info
+ERROR 5 Invalid value part of 'by-xxx-value' selector.
+
+GET 6 subscriber
+ERROR 6 Command not present.
+GET 7 subscriber.
+ERROR 7 Command not present.
+GET 8 subscriber.by-nonsense
+ERROR 8 Command not present.
+GET 9 subscriber.by-nonsense-
+ERROR 9 Command not present.
+GET 10 subscriber.by-nonsense-123456
+ERROR 10 Command not present.
+GET 11 subscriber.by-nonsense-123456.
+ERROR 11 Command not present.
+GET 12 subscriber.by-imsi-
+ERROR 12 Command not present.
+GET 13 subscriber.by-imsi-.
+ERROR 13 Command not present.
+GET 14 subscriber.by-imsi-901990000000003
+ERROR 14 Command not present.
+GET 15 subscriber.by-imsi-901990000000003.
+ERROR 15 Command not present.
+
+GET 16 subscriber.by-nonsense-123456.info
+ERROR 16 Not a known subscriber 'by-xxx-' selector.
+GET 17 subscriber.by-123456.info
+ERROR 17 Not a known subscriber 'by-xxx-' selector.
+
+GET 18 subscriber.by-imsi-.info
+ERROR 18 Invalid value part of 'by-xxx-value' selector.
+GET 19 subscriber.by-imsi--.info
+ERROR 19 Invalid value part of 'by-xxx-value' selector.
+
+GET 20 subscriber.by-imsi-12345678901234567.info
+ERROR 20 Invalid value part of 'by-xxx-value' selector.
+GET 21 subscriber.by-imsi-12345.info
+ERROR 21 Invalid value part of 'by-xxx-value' selector.
+GET 22 subscriber.by-imsi-1234567890123456.info
+ERROR 22 Invalid value part of 'by-xxx-value' selector.
+
+GET 23 subscriber.by-id-99999999999999999999999999.info
+ERROR 23 Invalid value part of 'by-xxx-value' selector.
+GET 24 subscriber.by-id-9223372036854775807.info
+ERROR 24 No such subscriber.
+GET 25 subscriber.by-id-9223372036854775808.info
+ERROR 25 Invalid value part of 'by-xxx-value' selector.
+GET 26 subscriber.by-id--1.info
+ERROR 26 No such subscriber.
+GET 27 subscriber.by-id--9223372036854775808.info
+ERROR 27 No such subscriber.
+GET 28 subscriber.by-id--9223372036854775809.info
+ERROR 28 Invalid value part of 'by-xxx-value' selector.
+
+GET 29 subscriber.by-id-1+1.info
+ERROR 29 GET variable contains invalid characters
+GET 30 subscriber.by-id--.info
+ERROR 30 Invalid value part of 'by-xxx-value' selector.
+GET 31 subscriber.by-id-+1.info
+ERROR 31 GET variable contains invalid characters
+GET 32 subscriber.by-id-+-1.info
+ERROR 32 GET variable contains invalid characters
+GET 33 subscriber.by-id--+1.info
+ERROR 33 GET variable contains invalid characters
+GET 34 subscriber.by-id-++1.info
+ERROR 34 GET variable contains invalid characters
+GET 35 subscriber.by-id---1.info
+ERROR 35 Invalid value part of 'by-xxx-value' selector.
+
+GET 36 subscriber.by-id- 1.info
+ERROR 36 GET with trailing characters
+GET 37 subscriber.by-id-+ 1.info
+ERROR 37 GET variable contains invalid characters
+GET 38 subscriber.by-id-- 1.info
+ERROR 38 GET with trailing characters
+
+
+SET 39 subscriber.by-imsi-901990000000001.info foo
+ERROR 39 Read Only attribute
+SET 40 subscriber.by-imsi-901990000000001.info-aud foo
+ERROR 40 Read Only attribute
+SET 41 subscriber.by-imsi-901990000000001.info-all foo
+ERROR 41 Read Only attribute
+
+SET 42 subscriber.by-imsi-901990000000001.ps-enabled nonsense
+ERROR 42 Value failed verification.
+SET 43 subscriber.by-imsi-901990000000001.cs-enabled nonsense
+ERROR 43 Value failed verification.
+
+SET 44 subscriber.by-imsi-901990000000001.ps-enabled
+ERROR 44 SET incomplete
+SET 45 subscriber.by-imsi-901990000000001.cs-enabled
+ERROR 45 SET incomplete
+
+GET 46 subscriber.by-imsi-1234567890123456.ps-enabled
+ERROR 46 Invalid value part of 'by-xxx-value' selector.
+GET 47 subscriber.by-imsi-1234567890123456.cs-enabled
+ERROR 47 Invalid value part of 'by-xxx-value' selector.
diff --git a/tests/testsuite.at b/tests/testsuite.at
new file mode 100644
index 0000000..70ae7ae
--- /dev/null
+++ b/tests/testsuite.at
@@ -0,0 +1,38 @@
+AT_INIT
+AT_BANNER([Regression tests.])
+
+AT_SETUP([auc])
+AT_KEYWORDS([auc])
+cat $abs_srcdir/auc/auc_test.ok > expout
+cat $abs_srcdir/auc/auc_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/auc/auc_test], [], [expout], [experr])
+AT_CLEANUP
+
+AT_SETUP([auc_ts_55_205_test_sets])
+AT_KEYWORDS([auc_ts_55_205_test_sets])
+cat $abs_srcdir/auc/auc_ts_55_205_test_sets.ok > expout
+cat $abs_srcdir/auc/auc_ts_55_205_test_sets.err > experr
+AT_CHECK([$abs_top_builddir/tests/auc/auc_ts_55_205_test_sets], [], [expout], [experr])
+AT_CLEANUP
+
+AT_SETUP([gsup])
+AT_KEYWORDS([gsup])
+cat $abs_srcdir/gsup/gsup_test.ok > expout
+cat $abs_srcdir/gsup/gsup_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/gsup/gsup_test], [], [expout], [experr])
+AT_CLEANUP
+
+AT_SETUP([gsup_server])
+AT_KEYWORDS([gsup_server])
+cat $abs_srcdir/gsup_server/gsup_server_test.ok > expout
+cat $abs_srcdir/gsup_server/gsup_server_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/gsup_server/gsup_server_test], [], [expout], [experr])
+AT_CLEANUP
+
+AT_SETUP([db])
+AT_KEYWORDS([db])
+cat $abs_srcdir/db/db_test.ok > expout
+cat $abs_srcdir/db/db_test.err > experr
+sqlite3 db_test.db < $abs_top_srcdir/sql/hlr.sql
+AT_CHECK([$abs_top_builddir/tests/db/db_test], [], [expout], [experr])
+AT_CLEANUP