diff --git a/tests/atlocal.in b/tests/atlocal.in
index 55f9333ee..023337b5c 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -168,6 +168,9 @@ fi
 # Set HAVE_TCPDUMP
 find_command tcpdump
 
+# Set HAVE_SIPP
+find_command sipp
+
 CURL_OPT="-g -v --max-time 1 --retry 2 --retry-delay 1 --connect-timeout 1"
 
 # Turn off proxies.
diff --git a/tests/automake.mk b/tests/automake.mk
index 18698ebc3..fc2efe255 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -94,6 +94,7 @@ TESTSUITE_AT = \
 	tests/vlog.at \
 	tests/vtep-ctl.at \
 	tests/auto-attach.at \
+	tests/conntrack-sip.at \
 	tests/ovn.at \
 	tests/ovn-northd.at \
 	tests/ovn-nbctl.at \
@@ -335,6 +336,7 @@ tests_ovstest_SOURCES = \
 	tests/test-ccmap.c \
 	tests/test-cmap.c \
 	tests/test-conntrack.c \
+	tests/test-sip.c \
 	tests/test-csum.c \
 	tests/test-flows.c \
 	tests/test-hash.c \
@@ -405,6 +407,11 @@ PYCOV_CLEAN_FILES += $(CHECK_PYFILES:.py=.py,cover) .coverage
 
 FLAKE8_PYFILES += $(CHECK_PYFILES)
 
+# SIPP scenarios
+EXTRA_DIST += \
+	tests/sipp/uac_happy_case_scenario.xml \
+	tests/sipp/uas_happy_case_scenario.xml
+
 if HAVE_OPENSSL
 TESTPKI_FILES = \
 	tests/testpki-cacert.pem \
@@ -446,4 +453,5 @@ CLEAN_LOCAL += clean-pki
 clean-pki:
 	rm -f tests/pki/stamp
 	rm -rf tests/pki
+
 endif
diff --git a/tests/conntrack-sip.at b/tests/conntrack-sip.at
new file mode 100644
index 000000000..17eff915c
--- /dev/null
+++ b/tests/conntrack-sip.at
@@ -0,0 +1,7 @@
+AT_BANNER([conntrack-sip unit tests])
+
+AT_SETUP([conntrack-sip])
+AT_KEYWORDS([conntrack-sip])
+AT_CHECK(ovstest test-sip, [], [ignore], [ignore])
+
+AT_CLEANUP
diff --git a/tests/sipp/uac_happy_case_scenario.xml b/tests/sipp/uac_happy_case_scenario.xml
new file mode 100644
index 000000000..ee728dfc1
--- /dev/null
+++ b/tests/sipp/uac_happy_case_scenario.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "sipp.dtd">
+
+<!-- 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, write to the                      -->
+<!-- Free Software Foundation, Inc.,                                    -->
+<!-- 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA             -->
+<!--                                                                    -->
+<!--                 Sipp default 'uac' scenario.                       -->
+<!--                                                                    -->
+
+<scenario name="Basic Sipstone UAC">
+  <!-- In client mode (sipp placing calls), the Call-ID MUST be         -->
+  <!-- generated by sipp. To do so, use [call_id] keyword.                -->
+  <!--
+      Content-Type: application/sdp
+  -->
+  <send retrans="500">
+    <![CDATA[
+
+      INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>
+      Call-ID: [call_id]
+      CSeq: 1 INVITE
+      Contact: sip:sipp@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Type: application/sdp
+      Content-Length: [len]
+
+      v=0
+      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+      s=-
+      c=IN IP[media_ip_type] [media_ip]
+      t=0 0
+      m=audio [rtpstream_audio_port] RTP/AVP 0
+      a=rtpmap:0 PCMU/8000
+
+    ]]>
+  </send>
+
+  <recv response="100"
+        optional="true">
+  </recv>
+
+  <recv response="180" optional="true">
+  </recv>
+
+  <recv response="183" optional="true">
+  </recv>
+
+  <!-- By adding rrs="true" (Record Route Sets), the route sets         -->
+  <!-- are saved and used for following messages sent. Useful to test   -->
+  <!-- against stateful SIP proxies/B2BUAs.                             -->
+  <recv response="200" rtd="true">
+  </recv>
+
+  <!-- Packet lost can be simulated in any send/recv message by         -->
+  <!-- by adding the 'lost = "10"'. Value can be [1-100] percent.       -->
+  <send>
+    <![CDATA[
+
+      ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 1 ACK
+      Contact: sip:sipp@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <nop>
+    <action>
+      <exec rtp_stream="/home/lam/Downloads/a2002011001-e02-16kHz.wav"/>
+    </action>
+  </nop>
+
+  <!-- This delay can be customized by the -d command-line option       -->
+  <!-- or by adding a 'milliseconds = "value"' option here.             -->
+  <pause/>
+
+  <!-- The 'crlf' option inserts a blank line in the statistics report. -->
+  <send retrans="500">
+    <![CDATA[
+
+      BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 2 BYE
+      Contact: sip:sipp@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv response="200" crlf="true">
+  </recv>
+
+  <!-- definition of the response time repartition table (unit is ms)   -->
+  <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
+
+  <!-- definition of the call length repartition table (unit is ms)     -->
+  <CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
+
+</scenario>
diff --git a/tests/sipp/uas_happy_case_scenario.xml b/tests/sipp/uas_happy_case_scenario.xml
new file mode 100644
index 000000000..beb2110c2
--- /dev/null
+++ b/tests/sipp/uas_happy_case_scenario.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "sipp.dtd">
+
+<!-- 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, write to the                      -->
+<!-- Free Software Foundation, Inc.,                                    -->
+<!-- 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA             -->
+<!--                                                                    -->
+<!--                 Sipp default 'uas' scenario.                       -->
+<!--                                                                    -->
+
+<scenario name="Basic UAS responder">
+  <!-- By adding rrs="true" (Record Route Sets), the route sets         -->
+  <!-- are saved and used for following messages sent. Useful to test   -->
+  <!-- against stateful SIP proxies/B2BUAs.                             -->
+  <recv request="INVITE" crlf="true">
+  </recv>
+
+  <!-- The '[last_*]' keyword is replaced automatically by the          -->
+  <!-- specified header if it was present in the last message received  -->
+  <!-- (except if it was a retransmission). If the header was not       -->
+  <!-- present or if no message has been received, the '[last_*]'       -->
+  <!-- keyword is discarded, and all bytes until the end of the line    -->
+  <!-- are also discarded.                                              -->
+  <!--                                                                  -->
+  <!-- If the specified header was present several times in the         -->
+  <!-- message, all occurences are concatenated (CRLF seperated)        -->
+  <!-- to be used in place of the '[last_*]' keyword.                   -->
+
+  <!-- No need for 180
+  <send>
+    <![CDATA[
+
+      SIP/2.0 180 Ringing
+      [last_Via:]
+      [last_From:]
+      [last_To:];tag=[pid]SIPpTag01[call_number]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+  -->
+
+  <send retrans="500">
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:];tag=[pid]SIPpTag01[call_number]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Type: application/sdp
+      Content-Length: [len]
+
+      v=0
+      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+      s=-
+      c=IN IP[media_ip_type] [media_ip]
+      t=0 0
+      m=audio [rtpstream_audio_port] RTP/AVP 0
+      a=rtpmap:0 PCMU/8000
+
+    ]]>
+  </send>
+
+  <recv request="ACK"
+        rtd="true"
+        crlf="true">
+  </recv>
+
+  <nop>
+    <action>
+      <exec rtp_stream="/home/lam/Downloads/a2002011001-e02-16kHz.wav"/>
+    </action>
+  </nop>
+
+  <!-- This delay can be customized by the -d command-line option       -->
+  <!-- or by adding a 'milliseconds = "value"' option here.             -->
+  <pause/>
+
+  <recv request="BYE">
+  </recv>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <!-- Keep the call open for a while in case the 200 is lost to be     -->
+  <!-- able to retransmit it if we receive the BYE again.               -->
+  <timewait milliseconds="4000"/>
+
+
+  <!-- definition of the response time repartition table (unit is ms)   -->
+  <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
+
+  <!-- definition of the call length repartition table (unit is ms)     -->
+  <CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
+
+</scenario>
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index dbd56405d..e8dba721a 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -2870,6 +2870,77 @@ udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=<cleared>,dport=<cleared>),reply=(src=
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
 
+dnl Setups two namespaces, at_ns0 and at_ns1, connected to a bridge, br0, and
+dnl runs sipp in both in the following form:
+dnl - at_ns0 loads an UAS scenario and waits for requests;
+dnl - at_ns1 loads an UAC scenario, which will connect to at_ns0.
+dnl
+dnl Aside from RTP (which is over UDP), this tests sends SIP over TCP over
+dnl IPv4 and no NAT is performed.
+AT_SETUP([conntrack - SIP over TCP over IPv4])
+AT_SKIP_IF([test $HAVE_SIPP = no])
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_ALG()
+
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.0.1.10/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 00:00:00:00:01:10])
+ADD_VETH(p1, at_ns1, br0, "10.0.2.10/24")
+NS_CHECK_EXEC([at_ns1], [ip link set dev p1 address 00:00:00:00:02:10])
+NS_CHECK_EXEC([at_ns0], [ip route add 10.0.2.0/24 via 10.0.1.10 dev p0])
+NS_CHECK_EXEC([at_ns1], [ip route add 10.0.1.0/24 via 10.0.2.10 dev p1])
+
+dnl Allow any traffic from at_ns1->at_ns0.
+dnl Only allow return traffic from at_ns0->at_ns1 (and arp, icmp).
+AT_DATA([flows.txt], [dnl
+dnl Track all IPv4 traffic
+table=0,priority=1,action=drop
+table=0,priority=10,arp,action=normal
+table=0,priority=10,icmp,action=normal
+table=0,priority=100,in_port=2,tcp,action=ct(alg=sip,commit),1
+table=0,priority=100,in_port=1,tcp,action=ct(table=1)
+table=0,priority=100,in_port=2,udp,action=ct(table=2)
+table=0,priority=100,in_port=1,udp,action=ct(table=2)
+dnl
+dnl Table 1
+dnl
+dnl Allow new TCPv4 TCP connections.
+table=1,in_port=1,tcp,ct_state=+trk+est,action=2
+table=1,in_port=1,tcp,ct_state=+trk+rel,action=2
+dnl
+dnl Table 2
+dnl
+dnl Allow RTP (UDP) connections through.
+table=2,in_port=1,udp,ct_state=+rel,action=2
+table=2,in_port=2,udp,ct_state=+rel,action=1
+])
+
+AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
+
+NETNS_DAEMONIZE([at_ns0], [sipp -t t1 -mi 10.0.1.10 -m 1 -d 20000 -sf $srcdir/sipp/uas_happy_case_scenario.xml], [sipp_uas.pid])
+OVS_WAIT_UNTIL([ip netns exec at_ns0 netstat -l | grep sip])
+
+dnl Traffic between at_ns1<->at_ns0 should now start flowing.
+NS_CHECK_EXEC([at_ns1], [sipp 10.0.1.10 -t t1 -rp 1s -m 1 -d 30000 -mi 10.0.2.10 -sf $srcdir/sipp/uac_happy_case_scenario.xml
+], [0], [stdout], [stderr])
+
+dnl Wait until uas in at_ns0 is done processing all calls
+dnl sipp in at_ns1 (running as uac) is finished with the calls, and sipp in
+dnl at_ns0 (running as uas) should be finished as well. If not, then something
+dnl is not right and the test should fail.
+OVS_WAIT_WHILE([ip netns exec at_ns0 netstat -l | grep sip])
+
+dnl Discards CLOSE_WAIT and CLOSING
+AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.0.1.10)], [0], [dnl
+tcp,orig=(src=10.0.2.10,dst=10.0.1.10,sport=<cleared>,dport=<cleared>),reply=(src=10.0.1.10,dst=10.0.2.10,sport=<cleared>,dport=<cleared>),protoinfo=(state=<cleared>),helper=sip
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_BANNER([conntrack - NAT])
 
 AT_SETUP([conntrack - simple SNAT])
diff --git a/tests/test-sip.c b/tests/test-sip.c
new file mode 100644
index 000000000..602f52671
--- /dev/null
+++ b/tests/test-sip.c
@@ -0,0 +1,327 @@
+/*
+ * Copyright (c) 2017 Avaya, Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+#undef NDEBUG
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "ovstest.h"
+#include "openvswitch/ofp-actions.h"
+#include "conntrack-sip.h"
+
+/* Count of tests run */
+static int num_tests = 0;
+
+static const struct eth_addr DUMMY_ETH_ADDR = ETH_ADDR_C(00,00,00,00,00,01);
+static const ovs_be32 DUMMY_IP_ADDR = 0x0a00020a;
+
+static char *valid_sdp =
+    "v=0\r\n"
+    "o=user1 53655765 2353687637 IN IP4 10.0.2.10\r\n"
+    "s=-\r\n"
+    "c=IN IP4 10.0.2.10\r\n"
+    "t=0 0\r\n"
+    "m=audio 8192 RTP/AVP 0\r\n"
+    "a=rtpmap:0 PCMU/8000\r\n";
+
+static char *invalid_sdp = "invalid sdp";
+
+static char *sip_invite =
+    "INVITE sip:service@10.0.1.10:5060 SIP/2.0\r\n"
+    "Via: SIP/2.0/TCP 10.0.2.10:5060;branch=z9hG4bK-26201-1-0\r\n"
+    "From: sipp <sip:sipp@10.0.2.10:5060>;tag=26201SIPpTag001\r\n"
+    "To: sut <sip:service@10.0.1.10:5060>\r\n"
+    "Call-ID: 1-26201@10.0.2.10\r\n"
+    "CSeq: 1 INVITE\r\n"
+    "Contact: sip:sipp@10.0.2.10:5060\r\n"
+    "Max-Forwards: 70\r\n"
+    "Content-Length:   0\r\n"
+    "\r\n";
+
+static char *sip_invite_with_sdp =
+    "INVITE sip:service@10.0.1.10:5060 SIP/2.0\r\n"
+    "Via: SIP/2.0/TCP 10.0.2.10:5060;branch=z9hG4bK-26201-1-0\r\n"
+    "From: sipp <sip:sipp@10.0.2.10:5060>;tag=26201SIPpTag001\r\n"
+    "To: sut <sip:service@10.0.1.10:5060>\r\n"
+    "Call-ID: 1-26201@10.0.2.10\r\n"
+    "CSeq: 1 INVITE\r\n"
+    "Contact: sip:sipp@10.0.2.10:5060\r\n"
+    "Max-Forwards: 70\r\n"
+    "Content-Type: application/sdp\r\n"
+    "Content-Length:   129\r\n"
+    "\r\n";
+
+static char *sip_invite_missing_cr =
+    "INVITE sip:service@10.0.1.10:5060 SIP/2.0\r\n"
+    "Via: SIP/2.0/TCP 10.0.2.10:5060;branch=z9hG4bK-26201-1-0\r\n"
+    "From: sipp <sip:sipp@10.0.2.10:5060>;tag=26201SIPpTag001\r\n"
+    "To: sut <sip:service@10.0.1.10:5060>\n"
+    "Call-ID: 1-26201@10.0.2.10\r\n"
+    "CSeq: 1 INVITE\r\n"
+    "Contact: sip:sipp@10.0.2.10:5060\r\n"
+    "Max-Forwards: 70\n"
+    "Content-Length:   0\r\n"
+    "\r\n";
+
+static void
+test_valid_sdp(void)
+{
+    struct sip_sdp *sdp;
+    sdp = sip_parse_sdp(valid_sdp, strlen(valid_sdp));
+
+    ovs_assert(sdp != NULL);
+    ovs_assert(sdp->orig == 0x0a02000a);
+    ovs_assert(sdp->conn == 0x0a02000a);
+    ovs_assert(sdp->port == 0x2000);
+
+    free(sdp);
+}
+
+static void
+test_invalid_sdp(void)
+{
+    struct sip_sdp *sdp;
+    sdp = sip_parse_sdp(invalid_sdp, strlen(invalid_sdp));
+
+    ovs_assert(sdp == NULL);
+
+    free(sdp);
+}
+
+static void
+test_parse_strt_ln(void)
+{
+    struct sip_strt_ln *strt_ln;
+    strt_ln = sip_parse_strt_ln(sip_invite, strlen(sip_invite));
+
+    ovs_assert(strt_ln != NULL);
+
+    ovs_assert(strt_ln->type == REQUEST_LINE);
+    ovs_assert(strt_ln->reqs_ln->mthd == INVITE);
+}
+
+static void
+test_parse_strt_ln_missing_cr(void)
+{
+    struct sip_strt_ln *strt_ln;
+    strt_ln = sip_parse_strt_ln(sip_invite_missing_cr,
+                                strlen(sip_invite_missing_cr));
+
+    ovs_assert(strt_ln != NULL);
+
+    ovs_assert(strt_ln->type == REQUEST_LINE);
+    ovs_assert(strt_ln->reqs_ln->mthd == INVITE);
+}
+
+static void *
+compose_dummy_ipv4(struct dp_packet *packet, uint8_t proto,
+                   ovs_be32 ipv4_src, ovs_be32 ipv4_dst,
+                   uint8_t tos, uint8_t ttl, int proto_hdr_len,
+                   int proto_len)
+{
+    struct ip_header *l3_hdr;
+    void *data;
+
+    l3_hdr = dp_packet_l3(packet);
+    l3_hdr->ip_proto = proto;
+    l3_hdr->ip_ihl_ver = IP_HEADER_LEN / 4;
+    l3_hdr->ip_tot_len = htons(IP_HEADER_LEN + proto_hdr_len + proto_len);
+    packet_set_ipv4(packet, ipv4_src, ipv4_dst, tos, ttl);
+    data = dp_packet_put_zeros(packet, proto_hdr_len);
+    dp_packet_set_l4(packet, data);
+    return data;
+}
+
+static void *
+compose_dummy_tcp(struct dp_packet *pkt, const struct eth_addr eth_src,
+                  const struct eth_addr eth_dst, ovs_be32 ipv4_src,
+                  ovs_be32 ipv4_dst, ovs_be16 port_src, ovs_be16 port_dst,
+                  int size)
+{
+    struct tcp_header *th;
+
+    eth_compose(pkt, eth_dst, eth_src, ETH_TYPE_IP, IP_HEADER_LEN);
+    th = compose_dummy_ipv4(pkt, IPPROTO_TCP, ipv4_src, ipv4_dst, 0, 1,
+                            TCP_HEADER_LEN, size);
+
+    packet_set_tcp_port(pkt, port_src, port_dst);
+    th->tcp_ctl = htons((TCP_HEADER_LEN / 4) << 12);
+
+    return th + TCP_HEADER_LEN;
+}
+
+/* Test sip_parse_tcp function against multiple cases, including:
+ * - A single TCP packet transporting a single SIP message;
+ * - A single TCP packet transporting multiple SIP messages;
+ * - A SIP message being carried across multiple TCP packets, i.e., a first
+ *   TCP packet arrives transporting an incomplete SIP message, followed by
+ *   another TCP packet wich carries the rest of the SIP message. */
+
+static void
+test_handle_single_tcp_single_sip(void)
+{
+    uint32_t stub[128 / 4];
+    struct dp_packet packet;
+
+    dp_packet_use_stub(&packet, stub, sizeof stub);
+    dp_packet_clear(&packet);
+
+    compose_dummy_tcp(&packet, DUMMY_ETH_ADDR, DUMMY_ETH_ADDR,
+                      htonl(DUMMY_IP_ADDR),htonl(DUMMY_IP_ADDR),
+                      htons(IPPORT_SIP), htons(IPPORT_SIP),
+                      strlen(sip_invite_with_sdp) + strlen(valid_sdp));
+    /* Compose an SIP message. */
+    dp_packet_put(&packet, sip_invite_with_sdp, strlen(sip_invite_with_sdp));
+    dp_packet_put(&packet, valid_sdp, strlen(valid_sdp));
+
+    struct ip_header *l3_hdr = dp_packet_l3(&packet);
+    struct tcp_header *th = dp_packet_l4(&packet);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+    size_t ip_hdr_len = (IP_IHL(l3_hdr->ip_ihl_ver) * 4);
+    size_t tcp_len = ntohs(l3_hdr->ip_tot_len) - ip_hdr_len - tcp_hdr_len;
+    /* Move to beginning of TCP payload, where the SIP payload is */
+    char *sip = ((char *) th) + tcp_hdr_len;
+    size_t sip_len = tcp_len;
+    struct ovs_list *out_msgs = NULL;
+
+    out_msgs = sip_parse_tcp(sip, sip_len);
+
+    ovs_assert(ovs_list_size(out_msgs) == 1);
+
+    struct sip_msg *out_msg;
+    struct sip_msg *next;
+    LIST_FOR_EACH_SAFE (out_msg, next, node, out_msgs) {
+        struct sip_strt_ln *strt_ln = out_msg->strt_ln;
+        sip_len = out_msg->bdy_len;
+
+        ovs_assert(strt_ln != NULL);
+        ovs_assert(strt_ln->type == REQUEST_LINE);
+        ovs_assert(strt_ln->reqs_ln->mthd == INVITE);
+        ovs_assert(sip_len == strlen(valid_sdp));
+
+        free_sip_msg(out_msg);
+    }
+    free(out_msgs);
+    dp_packet_uninit(&packet);
+}
+
+static void
+test_handle_single_tcp_multiple_sip(void)
+{
+    uint32_t stub[128 / 4];
+    struct dp_packet packet;
+
+    dp_packet_use_stub(&packet, stub, sizeof stub);
+    dp_packet_clear(&packet);
+
+    compose_dummy_tcp(&packet, DUMMY_ETH_ADDR, DUMMY_ETH_ADDR,
+                      htonl(DUMMY_IP_ADDR),htonl(DUMMY_IP_ADDR),
+                      htons(IPPORT_SIP), htons(IPPORT_SIP),
+                      2 * (strlen(sip_invite_with_sdp) + strlen(valid_sdp)));
+    /* Insert a first SIP message */
+    dp_packet_put(&packet, sip_invite_with_sdp, strlen(sip_invite_with_sdp));
+    dp_packet_put(&packet, valid_sdp, strlen(valid_sdp));
+    /* Insert a second SIP message */
+    dp_packet_put(&packet, sip_invite_with_sdp, strlen(sip_invite_with_sdp));
+    dp_packet_put(&packet, valid_sdp, strlen(valid_sdp));
+
+    struct ip_header *l3_hdr = dp_packet_l3(&packet);
+    struct tcp_header *th = dp_packet_l4(&packet);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+    size_t ip_hdr_len = (IP_IHL(l3_hdr->ip_ihl_ver) * 4);
+    size_t tcp_len = ntohs(l3_hdr->ip_tot_len) - ip_hdr_len - tcp_hdr_len;
+    /* Move to beginning of TCP payload, where the SIP payload is */
+    char *sip = ((char *) th) + tcp_hdr_len;
+    size_t sip_len = tcp_len;
+    struct ovs_list *out_msgs = NULL;
+
+    out_msgs = sip_parse_tcp(sip, sip_len);
+
+    ovs_assert(ovs_list_size(out_msgs) == 2);
+
+    struct sip_msg *out_msg;
+    struct sip_msg *next;
+    LIST_FOR_EACH_SAFE (out_msg, next, node, out_msgs) {
+        struct sip_strt_ln *strt_ln = out_msg->strt_ln;
+        sip_len = out_msg->bdy_len;
+
+        ovs_assert(strt_ln != NULL);
+        ovs_assert(strt_ln->type == REQUEST_LINE);
+        ovs_assert(strt_ln->reqs_ln->mthd == INVITE);
+        ovs_assert(sip_len == strlen(valid_sdp));
+        free_sip_msg(out_msg);
+    }
+    free(out_msgs);
+    dp_packet_uninit(&packet);
+}
+
+static void
+test_handle_single_tcp_incomplete_sip(void)
+{
+    uint32_t stub[128 / 4];
+    struct dp_packet packet;
+
+    dp_packet_use_stub(&packet, stub, sizeof stub);
+    dp_packet_clear(&packet);
+
+    compose_dummy_tcp(&packet, DUMMY_ETH_ADDR, DUMMY_ETH_ADDR,
+                      htonl(DUMMY_IP_ADDR),htonl(DUMMY_IP_ADDR),
+                      htons(IPPORT_SIP), htons(IPPORT_SIP),
+                      strlen(sip_invite_with_sdp));
+    dp_packet_put(&packet, sip_invite_with_sdp, strlen(sip_invite_with_sdp));
+
+    struct ip_header *l3_hdr = dp_packet_l3(&packet);
+    struct tcp_header *th = dp_packet_l4(&packet);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+    size_t ip_hdr_len = (IP_IHL(l3_hdr->ip_ihl_ver) * 4);
+    size_t tcp_len = ntohs(l3_hdr->ip_tot_len) - ip_hdr_len - tcp_hdr_len;
+    /* Move to beginning of TCP payload, where the SIP payload is */
+    char *sip = ((char *) th) + tcp_hdr_len;
+    size_t sip_len = tcp_len;
+    struct ovs_list *out_msgs = NULL;
+
+    out_msgs = sip_parse_tcp(sip, sip_len);
+
+    ovs_assert(ovs_list_size(out_msgs) == 0);
+
+    free(out_msgs);
+    dp_packet_uninit(&packet);
+}
+
+static void
+run_test(void (*function)(void))
+{
+    function();
+    num_tests++;
+    printf(".");
+}
+
+static void
+test_sip_main(int argc OVS_UNUSED, char *argv[] OVS_UNUSED)
+{
+    run_test(test_valid_sdp);
+    run_test(test_invalid_sdp);
+    run_test(test_parse_strt_ln);
+    run_test(test_parse_strt_ln_missing_cr);
+    run_test(test_handle_single_tcp_single_sip);
+    run_test(test_handle_single_tcp_multiple_sip);
+    run_test(test_handle_single_tcp_incomplete_sip);
+
+    printf("executed %d tests\n", num_tests);
+}
+OVSTEST_REGISTER("test-sip", test_sip_main);
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 15c385e2c..05297e7a7 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -71,6 +71,7 @@ m4_include([tests/rstp.at])
 m4_include([tests/vlog.at])
 m4_include([tests/vtep-ctl.at])
 m4_include([tests/auto-attach.at])
+m4_include([tests/conntrack-sip.at])
 m4_include([tests/ovn.at])
 m4_include([tests/ovn-northd.at])
 m4_include([tests/ovn-nbctl.at])
