From e70c796f1e27ed58b5b7f2f119286f695789d953 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Tue, 11 Jul 2023 00:14:17 -0500
Subject: [PATCH 01/13] [ADD] Added `__gen_from_to` in `SIPClient` for
 generating From/To original       headers. [ADD] Added `__gen_uri` in
 `SIPClient` for generating URIs for headers. [ADD] Added better error
 handling for database issues in `VoIPSocket`. [ADD] Added `get_database_dump`
 in `VoIPSocket`. [CHANGE] Renamed `SIPClient._gen_from_to` to
 `__gen_from_to_via_request`. [CHANGE] Renamed `SIPClient._gen_user_agent` to
 `__gen_user_agent`. [CHANGE] `SIPClient.invite` now also returns a
 `VoIPConnection`. [CHANGE] Changed signature of
 `SIPClient.trying_timeout_check` to include the          `VoIPConnection` so
 the timeout check can use the same dialog. [CHANGE] Changed `VoIPCall` to
 accept its `VoIPConnection` so it can interact          with its dialog.
 [FIX] Fixed SIP.recv error causing a crash it data was not received at all.
 [FIX] Fixed issue with `SIPClient.gen_invite` using malformed headers. [FIX]
 Fixed all instances of not capturing the return from `SIPClient.sendto`      
 causing multiple `VoIPConnection`s to be created for the same dialog. [FIX]
 Fixed issue with `VoIPSocket.determine_tags` returning a null local tag.
 [FIX] Fixed issue where `pyVoIP.VoIP` was not installing. [REMOVE] Removed
 commented code.

---
 pyVoIP/SIP/client.py    | 187 +++++++++++++++++++++++++++-------------
 pyVoIP/VoIP/__init__.py |   0
 pyVoIP/VoIP/call.py     |   2 +
 pyVoIP/VoIP/phone.py    |   3 +-
 pyVoIP/sock/sock.py     |  60 +++++++++----
 5 files changed, 176 insertions(+), 76 deletions(-)
 create mode 100644 pyVoIP/VoIP/__init__.py

diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index 104a7ab..a7bc584 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -24,6 +24,7 @@
 
 if TYPE_CHECKING:
     from pyVoIP import RTP
+    from pyVoIP.sock import VoIPConnection
 
 
 debug = pyVoIP.debug
@@ -98,9 +99,15 @@ def recv(self) -> None:
                 else:
                     debug(f"SIPParseError in SIP.recv: {type(e)}, {e}")
             except Exception as e:
-                debug(f"SIP.recv error: {type(e)}, {e}\n\n{str(raw, 'utf8')}")
-                if pyVoIP.DEBUG:
-                    raise
+                try:
+                    debug(
+                        f"SIP.recv error: {type(e)}, {e}\n\n{str(raw, 'utf8')}"
+                    )
+                except UnboundLocalError:
+                    debug("SIP.recv error: Unable to recv")
+                finally:
+                    if pyVoIP.DEBUG:
+                        raise
 
     def parse_message(self, message: SIPMessage) -> None:
         if message.type != SIPMessageType.REQUEST:
@@ -170,7 +177,6 @@ def start(self) -> None:
         from pyVoIP.sock.sock import VoIPSocket
 
         self.NSD = True
-        # self.s = socket.socket(socket.AF_INET, self.transport_mode.socket_type)
         self.s = VoIPSocket(
             self.transport_mode,
             self.bind_ip,
@@ -210,7 +216,7 @@ def sendto(self, request: str, address=None):
     def send(self, request: str):
         return self.s.send(request.encode("utf8"))
 
-    def _gen_from_to(
+    def __gen_from_to_via_request(
         self,
         request: SIPMessage,
         hdr: str,
@@ -230,10 +236,57 @@ def _gen_from_to(
 
         if tag:
             return f"{ret} <{uri}>;tag={tag}\r\n"
-        else:
-            return f"{ret} <{uri}>\r\n"
+        return f"{ret} <{uri}>\r\n"
+
+    def __gen_from_to(
+        self,
+        header_type: str,
+        user: str,
+        host: str,
+        method="sip",
+        display_name: Optional[str] = None,
+        password: Optional[str] = None,
+        port=5060,
+        uri_params: Optional[str] = None,
+        header_parms: Optional[str] = None,
+    ) -> str:
+        header_type = header_type.capitalize()
+
+        assert header_type in ["To", "From"], "header_type must be To or From"
+        assert (
+            display_name is None or '"' not in display_name
+        ), f'{display_name=} cannot contain a `"`'
 
-    def _gen_user_agent(self) -> str:
+        uri = self.__gen_uri(method, user, host, password, port, uri_params)
+        display_name = f'"{display_name}" ' if display_name else ""
+        header_parms = f"{header_parms}" if header_parms else ""
+        return f"{header_type}: {display_name}<{uri}>{header_parms}\r\n"
+
+    def __gen_uri(
+        self,
+        method: str,
+        user: str,
+        host: str,
+        password: Optional[str] = None,
+        port=5060,
+        params: Optional[str] = None,
+    ) -> str:
+        method = method.lower()
+
+        assert method in ["sip", "sips"], "method must be sip or sips"
+        assert (
+            type(user) is str and len(user) > 0
+        ), "User must be a non-empty string"
+        assert (
+            type(host) is str and len(host) > 0
+        ), "User must be a non-empty string"
+
+        password = f":{password}" if password else ""
+        port_str = f":{port}" if port != 5060 else ""
+        params = params if params else ""
+        return f"{method}:{user}{password}@{host}{port_str}{params}"
+
+    def __gen_user_agent(self) -> str:
         return f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
 
     def gen_call_id(self) -> str:
@@ -261,14 +314,16 @@ def gen_sip_version_not_supported(self, request: SIPMessage) -> str:
         response = "SIP/2.0 505 SIP Version Not Supported\r\n"
         response += self._gen_response_via_header(request)
         response += f"From: {request.headers['From']['raw']}\r\n"
-        response += self._gen_from_to(request, "To", self.gen_tag())
+        response += self.__gen_from_to_via_request(
+            request, "To", self.gen_tag()
+        )
         response += f"Call-ID: {request.headers['Call-ID']}\r\n"
         response += (
             f"CSeq: {request.headers['CSeq']['check']} "
             + f"{request.headers['CSeq']['method']}\r\n"
         )
         response += f"Contact: {request.headers['Contact']['raw']}\r\n"
-        response += self._gen_user_agent()
+        response += self.__gen_user_agent()
         response += 'Warning: 399 GS "Unable to accept call"\r\n'
         response += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         response += "Content-Length: 0\r\n\r\n"
@@ -308,7 +363,7 @@ def gen_digest(
         password = credentials["password"]
         nonce = request.authentication["nonce"]
         method = request.headers["CSeq"]["method"]
-        uri = f"sip:{self.server};transport={self.transport_mode}"
+        uri = f"sip:{self.server};transport={self.transport_mode}"  # TODO: Fix TLS
         algo = request.authentication.get("algorithm", "md5").lower()
         if algo in ["sha512-256", "sha512-256-sess"]:
             hash_func = self._hash_sha512_256
@@ -458,7 +513,7 @@ def gen_first_request(self, deregister=False) -> str:
         regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
         regRequest += "Max-Forwards: 70\r\n"
         regRequest += "Allow-Events: org.3gpp.nwinitdereg\r\n"
-        regRequest += self._gen_user_agent()
+        regRequest += self.__gen_user_agent()
         # Supported: 100rel, replaces, from-change, gruu
         regRequest += (
             "Expires: "
@@ -495,7 +550,7 @@ def gen_subscribe(self, response: SIPMessage) -> str:
             + f'"<urn:uuid:{self.urnUUID}>"\r\n'
         )
         subRequest += "Max-Forwards: 70\r\n"
-        subRequest += self._gen_user_agent()
+        subRequest += self.__gen_user_agent()
         subRequest += f"Expires: {self.default_expires * 2}\r\n"
         subRequest += "Event: message-summary\r\n"
         subRequest += "Accept: application/simple-message-summary\r\n"
@@ -535,7 +590,7 @@ def gen_register(self, request: SIPMessage, deregister=False) -> str:
         regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
         regRequest += "Max-Forwards: 70\r\n"
         regRequest += "Allow-Events: org.3gpp.nwinitdereg\r\n"
-        regRequest += self._gen_user_agent()
+        regRequest += self.__gen_user_agent()
         regRequest += (
             "Expires: "
             + f"{self.default_expires if not deregister else 0}\r\n"
@@ -550,7 +605,9 @@ def gen_busy(self, request: SIPMessage) -> str:
         response = "SIP/2.0 486 Busy Here\r\n"
         response += self._gen_response_via_header(request)
         response += f"From: {request.headers['From']['raw']}\r\n"
-        response += self._gen_from_to(request, "To", self.gen_tag())
+        response += self.__gen_from_to_via_request(
+            request, "To", self.gen_tag()
+        )
         response += f"Call-ID: {request.headers['Call-ID']}\r\n"
         response += (
             f"CSeq: {request.headers['CSeq']['check']} "
@@ -558,7 +615,7 @@ def gen_busy(self, request: SIPMessage) -> str:
         )
         response += f"Contact: {request.headers['Contact']['raw']}\r\n"
         # TODO: Add Supported
-        response += self._gen_user_agent()
+        response += self.__gen_user_agent()
         response += 'Warning: 399 GS "Unable to accept call"\r\n'
         response += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         response += "Content-Length: 0\r\n\r\n"
@@ -569,13 +626,15 @@ def gen_ok(self, request: SIPMessage) -> str:
         okResponse = "SIP/2.0 200 OK\r\n"
         okResponse += self._gen_response_via_header(request)
         okResponse += f"From: {request.headers['From']['raw']}\r\n"
-        okResponse += self._gen_from_to(request, "To", self.gen_tag())
+        okResponse += self.__gen_from_to_via_request(
+            request, "To", self.gen_tag()
+        )
         okResponse += f"Call-ID: {request.headers['Call-ID']}\r\n"
         okResponse += (
             f"CSeq: {request.headers['CSeq']['check']} "
             + f"{request.headers['CSeq']['method']}\r\n"
         )
-        okResponse += self._gen_user_agent()
+        okResponse += self.__gen_user_agent()
         okResponse += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         okResponse += "Content-Length: 0\r\n\r\n"
 
@@ -586,7 +645,7 @@ def gen_ringing(self, request: SIPMessage) -> str:
         regRequest = "SIP/2.0 180 Ringing\r\n"
         regRequest += self._gen_response_via_header(request)
         regRequest += f"From: {request.headers['From']['raw']}\r\n"
-        regRequest += self._gen_from_to(request, "To", tag)
+        regRequest += self.__gen_from_to_via_request(request, "To", tag)
         regRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
         regRequest += (
             f"CSeq: {request.headers['CSeq']['check']} "
@@ -594,7 +653,7 @@ def gen_ringing(self, request: SIPMessage) -> str:
         )
         regRequest += f"Contact: {request.headers['Contact']['raw']}\r\n"
         # TODO: Add Supported
-        regRequest += self._gen_user_agent()
+        regRequest += self.__gen_user_agent()
         regRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         regRequest += "Content-Length: 0\r\n\r\n"
 
@@ -639,7 +698,7 @@ def gen_answer(
         regRequest = "SIP/2.0 200 OK\r\n"
         regRequest += self._gen_response_via_header(request)
         regRequest += f"From: {request.headers['From']['raw']}\r\n"
-        regRequest += self._gen_from_to(request, "To", tag)
+        regRequest += self.__gen_from_to_via_request(request, "To", tag)
         regRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
         regRequest += (
             f"CSeq: {request.headers['CSeq']['check']} "
@@ -650,7 +709,7 @@ def gen_answer(
             + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
         )
         # TODO: Add Supported
-        regRequest += self._gen_user_agent()
+        regRequest += self.__gen_user_agent()
         regRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         regRequest += "Content-Type: application/sdp\r\n"
         regRequest += f"Content-Length: {len(body)}\r\n\r\n"
@@ -694,7 +753,13 @@ def gen_invite(
         tag = self.gen_tag()
         self.tagLibrary[call_id] = tag
 
-        invRequest = f"INVITE sip:{number}@{self.server} SIP/2.0\r\n"
+        uri_method = (
+            "sips" if self.transport_mode == TransportMode.TLS else "sip"
+        )
+        to_uri = self.__gen_uri(
+            uri_method, number, self.server, port=self.port
+        )
+        invRequest = f"INVITE {to_uri} SIP/2.0\r\n"
         invRequest += (
             "Via: SIP/2.0/"
             + str(self.transport_mode)
@@ -702,17 +767,25 @@ def gen_invite(
             + f"{branch}\r\n"
         )
         invRequest += "Max-Forwards: 70\r\n"
-        invRequest += (
-            "Contact: "
-            + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
+        uri = self.__gen_uri(
+            uri_method, self.user, self.bind_ip, port=self.bind_port
+        )
+        invRequest += f"Contact: <{uri}>\r\n"
+        invRequest += self.__gen_from_to(
+            "To", number, self.server, port=self.port
+        )
+        invRequest += self.__gen_from_to(
+            "From",
+            self.user,
+            self.bind_ip,
+            port=self.bind_port,
+            header_parms=f";tag={tag}",
         )
-        invRequest += f"To: <sip:{number}@{self.server}>\r\n"
-        invRequest += f"From: <sip:{self.user}@{self.bind_ip}>;tag={tag}\r\n"
         invRequest += f"Call-ID: {call_id}\r\n"
         invRequest += f"CSeq: {self.inviteCounter.next()} INVITE\r\n"
         invRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         invRequest += "Content-Type: application/sdp\r\n"
-        invRequest += self._gen_user_agent()
+        invRequest += self.__gen_user_agent()
         invRequest += f"Content-Length: {len(body)}\r\n\r\n"
         invRequest += body
 
@@ -726,11 +799,13 @@ def _gen_bye_cancel(self, request: SIPMessage, cmd: str) -> str:
         _from = request.headers["From"]
         to = request.headers["To"]
         if request.headers["From"]["tag"] == tag:
-            byeRequest += self._gen_from_to(request, "From", tag)
+            byeRequest += self.__gen_from_to_via_request(request, "From", tag)
             byeRequest += f"To: {to['raw']}\r\n"
         else:
             byeRequest += f"To: {_from['raw']}\r\n"
-            byeRequest += self._gen_from_to(request, "To", tag, dsthdr="From")
+            byeRequest += self.__gen_from_to_via_request(
+                request, "To", tag, dsthdr="From"
+            )
         byeRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
         cseq = request.headers["CSeq"]["check"]
         byeRequest += f"CSeq: {cseq} {cmd}\r\n"
@@ -738,7 +813,7 @@ def _gen_bye_cancel(self, request: SIPMessage, cmd: str) -> str:
             "Contact: "
             + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
         )
-        byeRequest += self._gen_user_agent()
+        byeRequest += self.__gen_user_agent()
         byeRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
         byeRequest += "Content-Length: 0\r\n\r\n"
 
@@ -766,7 +841,7 @@ def gen_ack(self, request: SIPMessage) -> str:
         ackMessage += f'From: {display_name}<{_from["uri"]}>;tag={tag}\r\n'
         ackMessage += f"Call-ID: {request.headers['Call-ID']}\r\n"
         ackMessage += f"CSeq: {request.headers['CSeq']['check']} ACK\r\n"
-        ackMessage += self._gen_user_agent()
+        ackMessage += self.__gen_user_agent()
         ackMessage += "Content-Length: 0\r\n\r\n"
 
         return ackMessage
@@ -801,16 +876,16 @@ def invite(
         number: str,
         ms: Dict[int, Dict[int, "RTP.PayloadType"]],
         sendtype: "RTP.TransmitType",
-    ) -> Tuple[SIPMessage, str, int]:
+    ) -> Tuple[SIPMessage, str, int, "VoIPConnection"]:
         branch = "z9hG4bK" + self.gen_call_id()[0:25]
         call_id = self.gen_call_id()
         sess_id = self.sessID.next()
         invite = self.gen_invite(
             number, str(sess_id), ms, sendtype, branch, call_id
         )
-        self.sendto(invite)
+        conn = self.sendto(invite)
         debug("Invited")
-        response = SIPMessage(self.s.recv(8192))
+        response = SIPMessage(conn.recv(8192))
 
         while (
             response.status != SIPStatus(401)
@@ -822,7 +897,7 @@ def invite(
                 break
             debug(f"Received Response: {response.summary()}")
             self.parse_message(response)
-            response = SIPMessage(self.s.recv(8192))
+            response = SIPMessage(conn.recv(8192))
 
         debug(f"Received Response: {response.summary()}")
 
@@ -832,7 +907,7 @@ def invite(
             debug("Invite status OK")
             return SIPMessage(invite.encode("utf8")), call_id, sess_id
         ack = self.gen_ack(response)
-        self.sendto(ack)
+        conn.send(ack)
         debug("Acknowledged")
         auth = self.gen_authorization(response)
 
@@ -843,9 +918,9 @@ def invite(
             "\r\nContent-Length", f"\r\n{auth}Content-Length"
         )
 
-        self.sendto(invite)
+        conn.send(invite)
 
-        return SIPMessage(invite.encode("utf8")), call_id, sess_id
+        return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn
 
     def gen_message(
         self, number: str, body: str, ctype: str, branch: str, call_id: str
@@ -874,11 +949,11 @@ def message(
         branch = "z0hG4bK" + self.gen_call_id()[0:25]
         call_id = self.gen_call_id()
         msg = self.gen_message(number, body, ctype, branch, call_id)
-        self.sendto(msg)
+        conn = self.sendto(msg)
         debug("Message")
         auth = False
         while True:
-            response = SIPMessage(self.s.recv(8192))
+            response = SIPMessage(conn.recv(8192))
             debug(f"Received Response: {response.summary()}")
             self.parse_message(response)
             if response.status == SIPStatus(100):
@@ -894,7 +969,7 @@ def message(
                 msg = msg.replace(
                     "\r\nContent-Length", "\r\n{auth}Content-Length"
                 )
-                self.sendto(msg)
+                conn.send(msg)
                 continue
             if response.status == SIPStatus.OK:
                 break
@@ -905,14 +980,14 @@ def message(
     def bye(self, request: SIPMessage) -> None:
         message = self.gen_bye(request)
         # TODO: Handle bye to server vs. bye to connected client
-        self.sendto(
+        conn = self.sendto(
             message,
             (
                 request.headers["Contact"]["host"],
                 request.headers["Contact"]["port"],
             ),
         )
-        response = SIPMessage(self.s.recv(8192))
+        response = SIPMessage(conn.recv(8192))
         if response.status == SIPStatus(401):
             #  Requires password
             auth = self.gen_authorization(response)
@@ -920,13 +995,7 @@ def bye(self, request: SIPMessage) -> None:
                 "\r\nContent-Length", f"\r\n{auth}Content-Length"
             )
             # TODO: Handle bye to server vs. bye to connected client
-            self.sendto(
-                message,
-                (
-                    request.headers["Contact"]["host"],
-                    request.headers["Contact"]["port"],
-                ),
-            )
+            conn.send(message)
         else:
             debug("Received not a 401 on bye:")
             debug(response.summary())
@@ -942,7 +1011,7 @@ def deregister(self) -> bool:
         resp = conn.recv(8192)
 
         response = SIPMessage(resp)
-        response = self.trying_timeout_check(response)
+        response = self.trying_timeout_check(conn, response)
 
         if response.status == SIPStatus(401):
             # Unauthorized, likely due to being password protected.
@@ -982,7 +1051,7 @@ def register(self) -> bool:
         resp = conn.recv(8192)
 
         response = SIPMessage(resp)
-        response = self.trying_timeout_check(response)
+        response = self.trying_timeout_check(conn, response)
         first_response = response
 
         if response.status == SIPStatus(400):
@@ -998,7 +1067,7 @@ def register(self) -> bool:
             conn.send(regRequest)
             resp = conn.recv(8192)
             response = SIPMessage(resp)
-            response = self.trying_timeout_check(response)
+            response = self.trying_timeout_check(conn, response)
             if response.status == SIPStatus(401):
                 # At this point, it's reasonable to assume that
                 # this is caused by invalid credentials.
@@ -1077,13 +1146,15 @@ def subscribe(self, lastresponse: SIPMessage) -> None:
         # TODO: check if needed and maybe implement fully
 
         subRequest = self.gen_subscribe(lastresponse)
-        self.sendto(subRequest)
+        conn = self.sendto(subRequest)
 
-        response = SIPMessage(self.s.recv(8192))
+        response = SIPMessage(conn.recv(8192))
 
         debug(f'Got response to subscribe: {str(response.heading, "utf8")}')
 
-    def trying_timeout_check(self, response: SIPMessage) -> SIPMessage:
+    def trying_timeout_check(
+        self, conn: "VoIPConnection", response: SIPMessage
+    ) -> SIPMessage:
         """
         Some servers need time to process the response.
         When this happens, the first response you get from the server is
diff --git a/pyVoIP/VoIP/__init__.py b/pyVoIP/VoIP/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyVoIP/VoIP/call.py b/pyVoIP/VoIP/call.py
index 69e773f..be4975f 100644
--- a/pyVoIP/VoIP/call.py
+++ b/pyVoIP/VoIP/call.py
@@ -20,6 +20,7 @@
 
 if TYPE_CHECKING:
     from pyVoIP.VoIP.phone import VoIPPhone
+    from pyVoIP.sock.sock import VoIPConnection
 
 
 class CallState(Enum):
@@ -39,6 +40,7 @@ def __init__(
         request: SIP.SIPMessage,
         session_id: int,
         bind_ip: str,
+        conn: "VoIPConnection",
         ms: Optional[Dict[int, RTP.PayloadType]] = None,
         sendmode="sendonly",
     ):
diff --git a/pyVoIP/VoIP/phone.py b/pyVoIP/VoIP/phone.py
index 9cc5486..0620daf 100644
--- a/pyVoIP/VoIP/phone.py
+++ b/pyVoIP/VoIP/phone.py
@@ -330,7 +330,7 @@ def call(
                 medias[port][dynamic_int] = pt
                 dynamic_int += 1
         debug(f"Making call with {medias=}")
-        request, call_id, sess_id = self.sip.invite(
+        request, call_id, sess_id, conn = self.sip.invite(
             number, medias, RTP.TransmitType.SENDRECV
         )
         self.calls[call_id] = self.callClass(
@@ -341,6 +341,7 @@ def call(
             self.bind_ip,
             ms=medias,
             sendmode=self.sendmode,
+            conn=conn,
         )
 
         return self.calls[call_id]
diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index 0acc040..2197475 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -2,6 +2,7 @@
 from pyVoIP.types import KEY_PASSWORD, SOCKETS
 from pyVoIP.SIP import SIPMessage, SIPMessageType
 from pyVoIP.sock.transport import TransportMode
+import json
 import math
 import pyVoIP
 import socket
@@ -253,25 +254,49 @@ def __register_connection(self, connection: VoIPConnection) -> None:
         self.conns_lock.acquire()
         self.conns.append(connection)
         conn_id = len(self.conns) - 1
-        conn = self.buffer.cursor()
-        conn.execute(
-            """INSERT INTO "listening"
-                ("call_id", "local_tag", "remote_tag", "connection")
-                VALUES
-                (?, ?, ?, ?)""",
-            (
-                connection.call_id,
-                connection.local_tag,
-                connection.remote_tag,
-                conn_id,
-            ),
-        )
         try:
+            conn = self.buffer.cursor()
+            conn.execute(
+                """INSERT INTO "listening"
+                    ("call_id", "local_tag", "remote_tag", "connection")
+                    VALUES
+                    (?, ?, ?, ?)""",
+                (
+                    connection.call_id,
+                    connection.local_tag,
+                    connection.remote_tag,
+                    conn_id,
+                ),
+            )
             self.buffer.commit()
+        except sqlite3.IntegrityError as e:
+            e.add_note(
+                "Error is from registering connection for message: "
+                + f"{connection.message.summary()}"
+            )
+            e.add_note("Internal Database Dump:\n" + self.get_database_dump())
+            e.add_note(
+                f"({connection.call_id=}, {connection.local_tag=}, "
+                + f"{connection.remote_tag=}, {conn_id=})"
+            )
+            raise
         except sqlite3.OperationalError:
             pass
-        conn.close()
-        self.conns_lock.release()
+        finally:
+            conn.close()
+            self.conns_lock.release()
+
+    def get_database_dump(self) -> str:
+        conn = self.buffer.cursor()
+        ret = ""
+        try:
+            result = conn.execute('SELECT * FROM "listening";')
+            ret += "listening: " + json.dumps(result.fetchall()) + "\n\n"
+            result = conn.execute('SELECT * FROM "msgs";')
+            ret += "msgs: " + json.dumps(result.fetchall()) + "\n\n"
+        finally:
+            conn.close()
+            return ret
 
     def determine_tags(self, message: SIPMessage) -> Tuple[str, str]:
         """
@@ -289,7 +314,7 @@ def determine_tags(self, message: SIPMessage) -> Tuple[str, str]:
         from_port = from_header["port"]
         from_tag = from_header["tag"] if from_header["tag"] else None
 
-        if to_host == self.bind_ip and to_port == self.bind_port:
+        if to_host == self.bind_ip and to_port == self.bind_port and to_tag:
             return to_tag, from_tag
         elif from_host == self.bind_ip and from_port == self.bind_port:
             return from_tag, to_tag
@@ -359,7 +384,8 @@ def run(self) -> None:
             raw_message = data.decode("utf8")
             conn = self.buffer.cursor()
             conn.execute(
-                "INSERT INTO msgs (call_id, local_tag, remote_tag, msg) VALUES (?, ?, ?, ?)",
+                "INSERT INTO msgs (call_id, local_tag, remote_tag, msg) "
+                + "VALUES (?, ?, ?, ?)",
                 (call_id, local_tag, remote_tag, raw_message),
             )
             try:

From 6d2b2315617f9b051e3624aef0b8ecaa2ea69678 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Mon, 7 Aug 2023 04:13:44 -0500
Subject: [PATCH 02/13] [ADD] Added type annotations. [ADD] Added ability to
 close a `VoIPConnection`. [CHANGE] Cleaned up register and deregister.
 [CHANGE] Changed `trying_timeout_check` to `__receive` and changed usage.
 [CHANGE] `VoIPConnection.recv` now raises a timeout error for UDP. [FIX]
 Fixed listening table not updating properly on receiving the remote tag.

---
 pyVoIP/SIP/client.py | 206 ++++++++++++++++++++-----------------------
 pyVoIP/sock/sock.py  |  79 +++++++++++++++--
 2 files changed, 167 insertions(+), 118 deletions(-)

diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index a7bc584..be21190 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -213,7 +213,7 @@ def sendto(self, request: str, address=None):
             address = (self.server, self.port)
         return self.s.send(request.encode("utf8"))
 
-    def send(self, request: str):
+    def send(self, request: str) -> "VoIPConnection":
         return self.s.send(request.encode("utf8"))
 
     def __gen_from_to_via_request(
@@ -1005,54 +1005,75 @@ def cancel(self, request: SIPMessage) -> None:
         self.sendto(message)
 
     def deregister(self) -> bool:
-        firstRequest = self.gen_first_request(deregister=True)
-        conn = self.send(firstRequest)
+        first_request = self.gen_first_request(deregister=True)
+        conn = self.send(first_request)
 
-        resp = conn.recv(8192)
-
-        response = SIPMessage(resp)
-        response = self.trying_timeout_check(conn, response)
+        response = self.__receive(conn)
+        first_response = response
+        conn.close()  # Regardless of the response, the dialog is over
 
         if response.status == SIPStatus(401):
             # Unauthorized, likely due to being password protected.
-            regRequest = self.gen_register(response, deregister=True)
-            conn.send(regRequest)
-            resp = conn.recv(8192)
-            response = SIPMessage(resp)
-            if response.status == SIPStatus(401):
-                # At this point, it's reasonable to assume that
-                # this is caused by invalid credentials.
-                debug("Unauthorized")
-                raise InvalidAccountInfoError(
-                    "Invalid Username or "
-                    + "Password for SIP server "
-                    + f"{self.server}:"
-                    + f"{self.bind_port}"
-                )
-            elif response.status == SIPStatus(400):
-                # Bad Request
-                # TODO: implement
-                # TODO: check if broken connection can be brought back
-                # with new urn:uuid or reply with expire 0
-                self._handle_bad_request()
-
-        if response.status == SIPStatus(500):
-            time.sleep(5)
-            return self.deregister()
-
-        if response.status == SIPStatus.OK:
+            password_request = self.gen_register(response, deregister=True)
+            conn = self.send(password_request)
+            response = self.__receive(conn)
+            conn.close()
+
+        if response.status == SIPStatus(400):
+            # Bad Request
+            # TODO: implement
+            # TODO: check if broken connection can be brought back
+            # with new urn:uuid or reply with expire 0
+            self._handle_bad_request()
+
+        elif response.status == SIPStatus(407):
+            # Proxy Authentication Required
+            # TODO: implement
+            debug("Proxy auth required")
+
+        elif response.status == SIPStatus(500):
+            raise Exception("Received a 500 error when deregistering.")
+
+        elif response.status == SIPStatus.OK:
             return True
-        return False
 
-    def register(self) -> bool:
-        firstRequest = self.gen_first_request()
-        conn = self.send(firstRequest)
+        elif response.status == SIPStatus(401):
+            # At this point, it's reasonable to assume that
+            # this is caused by invalid credentials.
+            debug("=" * 50)
+            debug("Unauthorized deregister, SIP Message Log:\n")
+            debug("SENT")
+            debug(first_request)
+            debug("\nRECEIVED")
+            debug(first_response.summary())
+            debug("\nSENT (DO NOT SHARE THIS PACKET)")
+            debug(password_request)
+            debug("\nRECEIVED")
+            debug(response.summary())
+            debug("=" * 50)
+            raise InvalidAccountInfoError(
+                f"Invalid Username or Password for SIP server {self.server}:"
+                + f"{self.bind_port}"
+            )
 
-        resp = conn.recv(8192)
+        raise Exception(
+            f"Unable to deregister. Ended with {response.summary()}"
+        )
 
-        response = SIPMessage(resp)
-        response = self.trying_timeout_check(conn, response)
+    def register(self) -> bool:
+        first_request = self.gen_first_request()
+        conn = self.send(first_request)
+
+        response = self.__receive(conn)
         first_response = response
+        conn.close()  # Regardless of the response, the dialog is over
+
+        if response.status == SIPStatus(401):
+            # Unauthorized, likely due to being password protected.
+            password_request = self.gen_register(response)
+            conn = self.send(password_request)
+            response = self.__receive(conn)
+            conn.close()
 
         if response.status == SIPStatus(400):
             # Bad Request
@@ -1061,63 +1082,15 @@ def register(self) -> bool:
             # with new urn:uuid or reply with expire 0
             self._handle_bad_request()
 
-        if response.status == SIPStatus(401):
-            # Unauthorized, likely due to being password protected.
-            regRequest = self.gen_register(response)
-            conn.send(regRequest)
-            resp = conn.recv(8192)
-            response = SIPMessage(resp)
-            response = self.trying_timeout_check(conn, response)
-            if response.status == SIPStatus(401):
-                # At this point, it's reasonable to assume that
-                # this is caused by invalid credentials.
-                debug("=" * 50)
-                debug("Unauthorized, SIP Message Log:\n")
-                debug("SENT")
-                debug(firstRequest)
-                debug("\nRECEIVED")
-                debug(first_response.summary())
-                debug("\nSENT (DO NOT SHARE THIS PACKET)")
-                debug(regRequest)
-                debug("\nRECEIVED")
-                debug(response.summary())
-                debug("=" * 50)
-                raise InvalidAccountInfoError(
-                    "Invalid Username or "
-                    + "Password for SIP server "
-                    + f"{self.server}:"
-                    + f"{self.bind_port}"
-                )
-            elif response.status == SIPStatus(400):
-                # Bad Request
-                # TODO: implement
-                # TODO: check if broken connection can be brought back
-                # with new urn:uuid or reply with expire 0
-                self._handle_bad_request()
-
-        if response.status == SIPStatus(407):
+        elif response.status == SIPStatus(407):
             # Proxy Authentication Required
             # TODO: implement
             debug("Proxy auth required")
 
-        # TODO: This must be done more reliable
-        if response.status not in [
-            SIPStatus(400),
-            SIPStatus(401),
-            SIPStatus(407),
-        ]:
-            # Unauthorized
-            if response.status == SIPStatus(500):
-                time.sleep(5)
-                return self.register()
-            else:
-                # TODO: determine if needed here
-                self.parse_message(response)
-
-        debug(response.summary())
-        debug(response.raw)
+        elif response.status == SIPStatus(500):
+            raise Exception("Received a 500 error when registering.")
 
-        if response.status == SIPStatus.OK:
+        elif response.status == SIPStatus.OK:
             if self.NSD:
                 # self.subscribe(response)
                 self.registerThread = Timer(
@@ -1128,13 +1101,28 @@ def register(self) -> bool:
                 )
                 self.registerThread.start()
             return True
-        else:
+
+        elif response.status == SIPStatus(401):
+            # At this point, it's reasonable to assume that
+            # this is caused by invalid credentials.
+            debug("=" * 50)
+            debug("Unauthorized, SIP Message Log:\n")
+            debug("SENT")
+            debug(first_request)
+            debug("\nRECEIVED")
+            debug(first_response.summary())
+            debug("\nSENT (DO NOT SHARE THIS PACKET)")
+            debug(password_request)
+            debug("\nRECEIVED")
+            debug(response.summary())
+            debug("=" * 50)
             raise InvalidAccountInfoError(
-                "Invalid Username or Password for "
-                + f"SIP server {self.server}:"
+                f"Invalid Username or Password for SIP server {self.server}:"
                 + f"{self.bind_port}"
             )
 
+        raise Exception(f"Unable to register. Ended with {response.summary()}")
+
     def _handle_bad_request(self) -> None:
         # Bad Request
         # TODO: implement
@@ -1152,25 +1140,21 @@ def subscribe(self, lastresponse: SIPMessage) -> None:
 
         debug(f'Got response to subscribe: {str(response.heading, "utf8")}')
 
-    def trying_timeout_check(
-        self, conn: "VoIPConnection", response: SIPMessage
-    ) -> SIPMessage:
+    def __receive(self, conn: "VoIPConnection") -> SIPMessage:
         """
         Some servers need time to process the response.
         When this happens, the first response you get from the server is
         SIPStatus.TRYING. This while loop tries checks every second for an
-        updated response. It times out after 30 seconds.
+        updated response. It times out after 30 seconds with no response.
         """
-        start_time = time.monotonic()
-        while response.status == SIPStatus.TRYING:
-            if (time.monotonic() - start_time) >= self.register_timeout:
-                raise TimeoutError(
-                    f"Waited {self.register_timeout} seconds but server is "
-                    + "still TRYING"
-                )
-
-            ready = select.select([self.s], [], [], self.register_timeout)
-            if ready[0]:
-                resp = self.s.recv(8192)
-            response = SIPMessage(resp)
+        try:
+            response = SIPMessage(conn.recv(8128, self.register_timeout))
+            while response.status == SIPStatus.TRYING and self.NSD:
+                response = SIPMessage(conn.recv(8128, self.register_timeout))
+                time.sleep(1)
+        except TimeoutError:
+            raise TimeoutError(
+                f"Waited {self.register_timeout} seconds but the server is "
+                + "still TRYING or has not responded."
+            )
         return response
diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index 2197475..90350c6 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -71,9 +71,11 @@ def __find_remote_tag(self) -> None:
         )
         rows = result.fetchall()
         if rows:
+            print(f"Found remote: {rows[0][0]}")
             self.remote_tag = rows[0][0]
 
     def recv(self, nbytes: int, timeout=0) -> bytes:
+        timeout = time.monotonic() + timeout if timeout else math.inf
         if self.conn:
             # TODO: Timeout
             msg = None
@@ -86,16 +88,19 @@ def recv(self, nbytes: int, timeout=0) -> bytes:
                         connection=self, error=e, received=data
                     )
                     self.send(br)
+            if time.monotonic() <= timeout:
+                raise TimeoutError()
             debug(f"RECEIVED:\n{msg.summary()}")
             return data
         else:
             self.__find_remote_tag()
-            timeout = time.monotonic() + timeout if timeout else math.inf
             while time.monotonic() <= timeout and not self.sock.SD:
+                # print("Trying to receive")
+                # print(self.sock.get_database_dump())
                 conn = self.sock.buffer.cursor()
                 conn.row_factory = sqlite3.Row
                 sql = (
-                    """SELECT * FROM "msgs" WHERE "call_id" = ? AND "local_tag" = ?"""
+                    'SELECT * FROM "msgs" WHERE "call_id"=? AND "local_tag"=?'
                     + (""" AND "remote_tag" = ?""" if self.remote_tag else "")
                 )
                 bindings = (
@@ -107,15 +112,21 @@ def recv(self, nbytes: int, timeout=0) -> bytes:
                 row = result.fetchone()
                 if not row:
                     continue
-                conn.execute(
-                    """DELETE FROM "msgs" WHERE "id" = ?""", (row["id"],)
-                )
+                conn.execute('DELETE FROM "msgs" WHERE "id" = ?', (row["id"],))
                 try:
                     self.sock.buffer.commit()
                 except sqlite3.OperationalError:
                     pass
                 conn.close()
                 return row["msg"].encode("utf8")
+            if time.monotonic() <= timeout:
+                raise TimeoutError()
+
+    def close(self):
+        self.__find_remote_tag()
+        self.sock.deregister_connection(self)
+        if self.conn:
+            self.conn.close()
 
 
 class VoIPSocket(threading.Thread):
@@ -230,23 +241,41 @@ def __get_connection(
         result = conn.execute(
             """SELECT "connection" FROM "listening" WHERE
                 "call_id" = ?
-                AND "local_tag" = ?""",
+                AND "local_tag" = ?
+                AND "remote_tag" is NULL""",
             (call_id, local_tag),
         )
         rows = result.fetchall()
         if rows:
+            conn.execute(
+                """UPDATE "listening" SET
+                    "remote_tag" = ? WHERE
+                    "call_id" = ?
+                    AND "local_tag" = ?
+                    AND "remote_tag" is NULL""",
+                (remote_tag, call_id, local_tag),
+            )
             conn.close()
             return self.conns[rows[0][0]]
         # If we still didn't find one, maybe we got the local and remote wrong?
         result = conn.execute(
             """SELECT "connection" FROM "listening" WHERE
                 "call_id" = ?
-                AND "local_tag" = ?""",
+                AND "local_tag" = ?
+                AND "remote_tag" is NULL""",
             (call_id, remote_tag),
         )
         rows = result.fetchall()
         conn.close()
         if rows:
+            conn.execute(
+                """UPDATE "listening" SET
+                    "remote_tag" = ? WHERE
+                    "call_id" = ?
+                    AND "local_tag" = ?
+                    AND "remote_tag" is NULL""",
+                (local_tag, call_id, remote_tag),
+            )
             return self.conns[rows[0][0]]
         return None
 
@@ -286,6 +315,42 @@ def __register_connection(self, connection: VoIPConnection) -> None:
             conn.close()
             self.conns_lock.release()
 
+    def deregister_connection(self, connection: VoIPConnection) -> None:
+        self.conns_lock.acquire()
+        print(f"Deregistering {connection}")
+        print(f"{self.conns=}")
+        print(self.get_database_dump())
+        try:
+            conn = self.buffer.cursor()
+            result = conn.execute(
+                """SELECT "connection" FROM "listening"
+                    WHERE "call_id" = ? AND "local_tag" = ?
+                    AND "remote_tag" = ?""",
+                (
+                    connection.call_id,
+                    connection.local_tag,
+                    connection.remote_tag,
+                ),
+            )
+            row = result.fetchone()
+            conn_id = row[0]
+            """
+            Need to set to None to not change the indexes of any other conn
+            """
+            self.conns[conn_id] = None
+            conn.execute(
+                'DELETE FROM "listening" WHERE "connection" = ?', (conn_id,)
+            )
+            self.buffer.commit()
+        except sqlite3.OperationalError:
+            pass
+        finally:
+            conn.close()
+            print("Deregistered")
+            print(f"{self.conns=}")
+            print(self.get_database_dump())
+            self.conns_lock.release()
+
     def get_database_dump(self) -> str:
         conn = self.buffer.cursor()
         ret = ""

From 9b50dbd4342c12fba2e0d8b78c0301407eaa0dc0 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Mon, 7 Aug 2023 04:16:21 -0500
Subject: [PATCH 03/13] [FIX] Fixed flake8 error.

---
 pyVoIP/SIP/client.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index be21190..aada151 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -19,7 +19,6 @@
 import random
 import time
 import uuid
-import select
 
 
 if TYPE_CHECKING:

From 322757bf9a4380989822f11938c992ab24277add Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Mon, 7 Aug 2023 04:25:19 -0500
Subject: [PATCH 04/13] [FIX] Updated my flake8, fixed last flake8 error.

---
 pyVoIP/SIP/message.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyVoIP/SIP/message.py b/pyVoIP/SIP/message.py
index 0990f8e..5ab7368 100644
--- a/pyVoIP/SIP/message.py
+++ b/pyVoIP/SIP/message.py
@@ -368,7 +368,7 @@ def __get_tfc_header(self, data: str) -> TFC_HEADER:
         if direct:
             reg = regex.TO_FROM_DIRECT_MATCH
         match = reg.match(data)
-        if type(match) != regex.Match:
+        if match is None:
             raise SIPParseError(
                 "Regex failed to match To/From.\n\n"
                 + "Please open a GitHub Issue at "

From f4b6d271c5786bf43d7566c591bafdbd423d1846 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Sun, 15 Oct 2023 18:41:27 -0500
Subject: [PATCH 05/13] [ADD] Added container and volume remove to the Docker
 start script. [ADD] Added basic Network Address Translation (NAT) feature.
 [ADD] Added __gen_via in SIP.client. [ADD] Added __gen_contact in SIP.client.
 [ADD] Added some IPv6 handling. [CHANGE] Changed Docker start script to
 PowerShell. [FIX] Fixed not receiving replies due to network routing. [FIX]
 Fixed registration tests failing due to max contacts. [FIX] Fixed uri's not
 denoting sips when using TLS [FIX] Fixed Via headers from other hosts being
 incorrectly changed. [FIX] Fixed timeout errors being raised incorrectly.
 [FIX] Fixed deregister_connection for TCP and TLS sockets. [FIX] Fixed all
 registration tests.

---
 .github/workflows/pytest.yml  |   2 +-
 docker/settings/pjsip.conf    |   4 +-
 docker/start.bat              |   4 -
 docker/start.ps1              |   5 ++
 pyVoIP/SIP/client.py          | 146 +++++++++++++++++++---------------
 pyVoIP/VoIP/phone.py          |   6 ++
 pyVoIP/networking/__init__.py |   0
 pyVoIP/networking/nat.py      |  41 ++++++++++
 pyVoIP/sock/sock.py           |  11 +--
 tests/test_functionality.py   |  52 ++++++------
 10 files changed, 170 insertions(+), 101 deletions(-)
 delete mode 100644 docker/start.bat
 create mode 100644 docker/start.ps1
 create mode 100644 pyVoIP/networking/__init__.py
 create mode 100644 pyVoIP/networking/nat.py

diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 5d96984..85c40ec 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -33,7 +33,7 @@ jobs:
       - name: Start Docker
         run: |
           docker build docker -t pyvoip/tests
-          docker run -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
+          docker run --add-host host.docker.internal:host-gateway -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
       - name: pytest
         # run: pytest --check-func
         run: pytest
diff --git a/docker/settings/pjsip.conf b/docker/settings/pjsip.conf
index 9ab5789..be0391e 100644
--- a/docker/settings/pjsip.conf
+++ b/docker/settings/pjsip.conf
@@ -29,7 +29,7 @@ aors=nopass
 
 [nopass]
 type=aor
-max_contacts=1
+max_contacts=999
 
 [pass]
 type=endpoint
@@ -48,4 +48,4 @@ password=Testing123!
 
 [pass]
 type=aor
-max_contacts=1
+max_contacts=999
diff --git a/docker/start.bat b/docker/start.bat
deleted file mode 100644
index 4b36fbf..0000000
--- a/docker/start.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-@echo off
-docker rmi pyvoip/tests
-docker build . -t pyvoip/tests
-docker run -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
diff --git a/docker/start.ps1 b/docker/start.ps1
new file mode 100644
index 0000000..d97dddf
--- /dev/null
+++ b/docker/start.ps1
@@ -0,0 +1,5 @@
+docker stop $(docker ps -a -q)
+docker rm --force --volumes $(docker ps -a -q)
+docker rmi pyvoip/tests
+docker build . -t pyvoip/tests
+docker run --add-host host.docker.internal:host-gateway -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index aada151..366ab74 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -7,6 +7,7 @@
     InvalidAccountInfoError,
 )
 from pyVoIP.helpers import Counter
+from pyVoIP.networking.nat import NAT
 from pyVoIP.SIP.message import (
     SIPMessage,
     SIPStatus,
@@ -37,6 +38,9 @@ def __init__(
         user: str,
         credentials_manager: CredentialsManager,
         bind_ip="0.0.0.0",
+        bind_network="0.0.0.0/0",
+        hostname: Optional[str] = None,
+        remote_hostname: Optional[str] = None,
         bind_port=5060,
         call_callback: Optional[Callable[[SIPMessage], Optional[str]]] = None,
         transport_mode: TransportMode = TransportMode.UDP,
@@ -49,6 +53,7 @@ def __init__(
         self.port = port
         self.bind_ip = bind_ip
         self.bind_port = bind_port
+        self.nat = NAT(bind_ip, bind_network, hostname, remote_hostname)
         self.user = user
         self.credentials_manager = credentials_manager
         self.transport_mode = transport_mode
@@ -285,6 +290,31 @@ def __gen_uri(
         params = params if params else ""
         return f"{method}:{user}{password}@{host}{port_str}{params}"
 
+    def __gen_via(self, to: str, branch: str) -> str:
+        # SIP/2.0/ should still be the prefix even if using TLS per RFC 3261
+        # 8.1.1.7, as shown in RFC 5630 6.1
+        return (
+            "Via: "
+            + f"SIP/2.0/{str(self.transport_mode)}"
+            + f" {self.nat.get_host(to)}:{self.bind_port};branch={branch}\r\n"
+        )
+
+    def __gen_contact(
+        self,
+        method: str,
+        user: str,
+        host: str,
+        password: Optional[str] = None,
+        port=5060,
+        uriparams: Optional[str] = None,
+        params: list[str] = [],
+    ) -> str:
+        uri = self.__gen_uri(method, user, host, password, port, uriparams)
+        uri = f"<{uri}>"
+        if params:
+            uri += ";" + (";".join(params))
+        return f"Contact: {uri}\r\n"
+
     def __gen_user_agent(self) -> str:
         return f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"
 
@@ -484,12 +514,8 @@ def gen_urn_uuid(self) -> str:
 
     def gen_first_request(self, deregister=False) -> str:
         regRequest = f"REGISTER sip:{self.server}:{self.port} SIP/2.0\r\n"
-        regRequest += (
-            "Via: SIP/2.0/"
-            + str(self.transport_mode)
-            + f" {self.bind_ip}:{self.bind_port};"
-            + f"branch={self.gen_branch()};rport\r\n"
-        )
+        regRequest += self.__gen_via(self.server, self.gen_branch())
+
         regRequest += (
             f'From: "{self.user}" '
             + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>;tag="
@@ -501,13 +527,14 @@ def gen_first_request(self, deregister=False) -> str:
         )
         regRequest += f"Call-ID: {self.gen_call_id()}\r\n"
         regRequest += f"CSeq: {self.registerCounter.next()} REGISTER\r\n"
-        regRequest += (
-            "Contact: "
-            + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port};"
-            + "transport="
-            + str(self.transport_mode)
-            + ">;+sip.instance="
-            + f'"<urn:uuid:{self.urnUUID}>"\r\n'
+        method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
+        trans_mode = str(self.transport_mode)
+        regRequest += self.__gen_contact(
+            method,
+            self.user,
+            self.nat.get_host(self.server),
+            uriparams=f";transport={trans_mode}",
+            params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
         )
         regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
         regRequest += "Max-Forwards: 70\r\n"
@@ -525,12 +552,7 @@ def gen_first_request(self, deregister=False) -> str:
 
     def gen_subscribe(self, response: SIPMessage) -> str:
         subRequest = f"SUBSCRIBE sip:{self.user}@{self.server} SIP/2.0\r\n"
-        subRequest += (
-            "Via: SIP/2.0/"
-            + str(self.transport_mode)
-            + f" {self.bind_ip}:{self.bind_port};"
-            + f"branch={self.gen_branch()};rport\r\n"
-        )
+        subRequest += self.__gen_via(self.server, self.gen_branch())
         subRequest += (
             f'From: "{self.user}" '
             + f"<sip:{self.user}@{self.server}>;tag="
@@ -540,13 +562,14 @@ def gen_subscribe(self, response: SIPMessage) -> str:
         subRequest += f'Call-ID: {response.headers["Call-ID"]}\r\n'
         subRequest += f"CSeq: {self.subscribeCounter.next()} SUBSCRIBE\r\n"
         # TODO: check if transport is needed
-        subRequest += (
-            "Contact: "
-            + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port};"
-            + "transport="
-            + str(self.transport_mode)
-            + ">;+sip.instance="
-            + f'"<urn:uuid:{self.urnUUID}>"\r\n'
+        method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
+        trans_mode = str(self.transport_mode)
+        subRequest += self.__gen_contact(
+            method,
+            self.user,
+            self.nat.get_host(self.server),
+            uriparams=f";transport={trans_mode}",
+            params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
         )
         subRequest += "Max-Forwards: 70\r\n"
         subRequest += self.__gen_user_agent()
@@ -560,12 +583,7 @@ def gen_subscribe(self, response: SIPMessage) -> str:
 
     def gen_register(self, request: SIPMessage, deregister=False) -> str:
         regRequest = f"REGISTER sip:{self.server}:{self.port} SIP/2.0\r\n"
-        regRequest += (
-            "Via: SIP/2.0/"
-            + str(self.transport_mode)
-            + f" {self.bind_ip}:{self.bind_port};branch="
-            + f"{self.gen_branch()};rport\r\n"
-        )
+        regRequest += self.__gen_via(self.server, self.gen_branch())
         regRequest += (
             f'From: "{self.user}" '
             + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>;tag="
@@ -578,13 +596,14 @@ def gen_register(self, request: SIPMessage, deregister=False) -> str:
         call_id = request.headers.get("Call-ID", self.gen_call_id())
         regRequest += f"Call-ID: {call_id}\r\n"
         regRequest += f"CSeq: {self.registerCounter.next()} REGISTER\r\n"
-        regRequest += (
-            "Contact: "
-            + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port};"
-            + "transport="
-            + str(self.transport_mode)
-            + ">;+sip.instance="
-            + f'"<urn:uuid:{self.urnUUID}>"\r\n'
+        method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
+        trans_mode = str(self.transport_mode)
+        regRequest += self.__gen_contact(
+            method,
+            self.user,
+            self.nat.get_host(self.server),
+            uriparams=f";transport={trans_mode}",
+            params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
         )
         regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
         regRequest += "Max-Forwards: 70\r\n"
@@ -703,9 +722,12 @@ def gen_answer(
             f"CSeq: {request.headers['CSeq']['check']} "
             + f"{request.headers['CSeq']['method']}\r\n"
         )
-        regRequest += (
-            "Contact: "
-            + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
+        method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
+        trans_mode = str(self.transport_mode)
+        regRequest += self.__gen_contact(
+            method,
+            self.user,
+            self.nat.get_host(self.server),
         )
         # TODO: Add Supported
         regRequest += self.__gen_user_agent()
@@ -727,12 +749,12 @@ def gen_invite(
     ) -> str:
         # Generate body first for content length
         body = "v=0\r\n"
-        # TODO: Check IPv4/IPv6
         body += (
-            f"o=pyVoIP {sess_id} {int(sess_id)+2} IN IP4 {self.bind_ip}\r\n"
+            f"o=pyVoIP {sess_id} {int(sess_id)+2} IN IP"
+            + f"{self.nat.bind_ip.version} {self.bind_ip}\r\n"
         )
         body += f"s=pyVoIP {pyVoIP.__version__}\r\n"
-        body += f"c=IN IP4 {self.bind_ip}\r\n"  # TODO: Check IPv4/IPv6
+        body += f"c=IN IP{self.nat.bind_ip.version} {self.bind_ip}\r\n"
         body += "t=0 0\r\n"
         for x in ms:
             # TODO: Check AVP mode from request
@@ -759,24 +781,21 @@ def gen_invite(
             uri_method, number, self.server, port=self.port
         )
         invRequest = f"INVITE {to_uri} SIP/2.0\r\n"
-        invRequest += (
-            "Via: SIP/2.0/"
-            + str(self.transport_mode)
-            + f" {self.bind_ip}:{self.bind_port};branch="
-            + f"{branch}\r\n"
-        )
+        invRequest += self.__gen_via(self.server, branch)
         invRequest += "Max-Forwards: 70\r\n"
-        uri = self.__gen_uri(
-            uri_method, self.user, self.bind_ip, port=self.bind_port
+        method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
+        invRequest += self.__gen_contact(
+            method,
+            self.user,
+            self.nat.get_host(self.server),
         )
-        invRequest += f"Contact: <{uri}>\r\n"
         invRequest += self.__gen_from_to(
             "To", number, self.server, port=self.port
         )
         invRequest += self.__gen_from_to(
             "From",
             self.user,
-            self.bind_ip,
+            self.nat.get_host(self.server),
             port=self.bind_port,
             header_parms=f";tag={tag}",
         )
@@ -808,9 +827,11 @@ def _gen_bye_cancel(self, request: SIPMessage, cmd: str) -> str:
         byeRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
         cseq = request.headers["CSeq"]["check"]
         byeRequest += f"CSeq: {cseq} {cmd}\r\n"
-        byeRequest += (
-            "Contact: "
-            + f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
+        method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
+        byeRequest += self.__gen_contact(
+            method,
+            self.user,
+            self.nat.get_host(self.server),
         )
         byeRequest += self.__gen_user_agent()
         byeRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
@@ -852,9 +873,7 @@ def _gen_response_via_header(self, request: SIPMessage) -> str:
         via = ""
         for h_via in request.headers["Via"]:
             v_line = (
-                "Via: SIP/2.0/"
-                + str(self.transport_mode)
-                + " "
+                f"Via: {h_via['type']} "
                 + f'{h_via["address"][0]}:{h_via["address"][1]}'
             )
             if "branch" in h_via.keys():
@@ -925,10 +944,7 @@ def gen_message(
         self, number: str, body: str, ctype: str, branch: str, call_id: str
     ) -> str:
         msg = f"MESSAGE sip:{number}@{self.server} SIP/2.0\r\n"
-        msg += (
-            f"Via: SIP/2.0/{self.transport_mode} "
-            + f"{self.bind_ip}:{self.bind_port};branch={branch}\r\n"
-        )
+        msg += self.__gen_via(self.server, branch)
         msg += "Max-Forwards: 70\r\n"
         msg += f"To: <sip:{number}@{self.server}>\r\n"
         msg += (
diff --git a/pyVoIP/VoIP/phone.py b/pyVoIP/VoIP/phone.py
index 0620daf..9f5bde3 100644
--- a/pyVoIP/VoIP/phone.py
+++ b/pyVoIP/VoIP/phone.py
@@ -40,6 +40,9 @@ def __init__(
         user: str,
         credentials_manager: CredentialsManager,
         bind_ip="0.0.0.0",
+        bind_network="0.0.0.0/0",
+        hostname: Optional[str] = None,
+        remote_hostname: Optional[str] = None,
         bind_port=5060,
         transport_mode=TransportMode.UDP,
         cert_file: Optional[str] = None,
@@ -90,6 +93,9 @@ def __init__(
             user,
             credentials_manager,
             bind_ip=self.bind_ip,
+            bind_network=bind_network,
+            hostname=hostname,
+            remote_hostname=remote_hostname,
             bind_port=bind_port,
             call_callback=self.callback,
             transport_mode=self.transport_mode,
diff --git a/pyVoIP/networking/__init__.py b/pyVoIP/networking/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyVoIP/networking/nat.py b/pyVoIP/networking/nat.py
new file mode 100644
index 0000000..bea9de9
--- /dev/null
+++ b/pyVoIP/networking/nat.py
@@ -0,0 +1,41 @@
+from typing import Optional
+import ipaddress
+import socket
+
+
+class NATError(Exception):
+    pass
+
+
+class NAT:
+    def __init__(
+        self,
+        bind_ip: str,
+        network: str,
+        hostname: Optional[str] = None,
+        remote_hostname: Optional[str] = None,
+    ):
+        self.bind_ip = ipaddress.ip_address(bind_ip)
+        self.network = ipaddress.ip_network(network)
+        self.hostname = bind_ip if hostname is None else hostname
+        self.remote_hostname = remote_hostname
+
+    def get_host(self, host: str):
+        """Return the hostname another client needs to connect to us."""
+        try:
+            ip = ipaddress.ip_address(host)
+        except ValueError:
+            try:
+                ip = socket.gethostbyname(host)
+            except socket.gaierror:
+                raise NATError(f"Unable to resolve hostname {host}")
+
+        if ip in self.network:
+            return self.hostname
+        else:
+            if self.remote_hostname is not None:
+                return self.remote_hostname
+            raise NATError(
+                "No remote hostname specified, "
+                + "cannot provide a return path for remote hosts."
+            )
diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index 90350c6..958670c 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -88,7 +88,7 @@ def recv(self, nbytes: int, timeout=0) -> bytes:
                         connection=self, error=e, received=data
                     )
                     self.send(br)
-            if time.monotonic() <= timeout:
+            if time.monotonic() >= timeout:
                 raise TimeoutError()
             debug(f"RECEIVED:\n{msg.summary()}")
             return data
@@ -119,7 +119,7 @@ def recv(self, nbytes: int, timeout=0) -> bytes:
                     pass
                 conn.close()
                 return row["msg"].encode("utf8")
-            if time.monotonic() <= timeout:
+            if time.monotonic() >= timeout:
                 raise TimeoutError()
 
     def close(self):
@@ -316,6 +316,8 @@ def __register_connection(self, connection: VoIPConnection) -> None:
             self.conns_lock.release()
 
     def deregister_connection(self, connection: VoIPConnection) -> None:
+        if self.mode is not TransportMode.UDP:
+            return
         self.conns_lock.acquire()
         print(f"Deregistering {connection}")
         print(f"{self.conns=}")
@@ -365,7 +367,7 @@ def get_database_dump(self) -> str:
 
     def determine_tags(self, message: SIPMessage) -> Tuple[str, str]:
         """
-        Returns local_tag, remote_tag
+        Return local_tag, remote_tag
         """
         # TODO: Eventually NAT will be supported for people who don't have a SIP ALG
         # We will need to take that into account when determining the remote tag.
@@ -398,8 +400,7 @@ def determine_tags(self, message: SIPMessage) -> Tuple[str, str]:
             message.type == SIPMessageType.REQUEST and message.method != "ACK"
         ):
             return to_tag, from_tag
-        else:
-            return from_tag, to_tag
+        return from_tag, to_tag
 
     def bind(self, addr: Tuple[str, int]) -> None:
         self.s.bind(addr)
diff --git a/tests/test_functionality.py b/tests/test_functionality.py
index d36cd49..7e6f55b 100644
--- a/tests/test_functionality.py
+++ b/tests/test_functionality.py
@@ -13,6 +13,10 @@
 )
 REASON = "Not checking functionality"
 pyVoIP.set_tls_security(ssl.CERT_NONE)
+SERVER_HOST = "127.0.0.1"
+UDP_PORT = 5060
+TCP_PORT = 5061
+TLS_PORT = 5062
 
 
 @pytest.fixture
@@ -20,11 +24,11 @@ def phone():
     cm = CredentialsManager()
     cm.add("pass", "Testing123!")
     phone = VoIPPhone(
-        "127.0.0.1",
-        5060,
+        SERVER_HOST,
+        UDP_PORT,
         "pass",
         cm,
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
     )
     phone.start()
@@ -35,11 +39,11 @@ def phone():
 @pytest.fixture
 def nopass_phone():
     phone = VoIPPhone(
-        "127.0.0.1",
-        5060,
+        SERVER_HOST,
+        UDP_PORT,
         "nopass",
         CredentialsManager(),
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
     )
     phone.start()
@@ -52,11 +56,11 @@ def nopass_phone():
 @pytest.mark.skipif(TEST_CONDITION, reason=REASON)
 def test_nopass():
     phone = VoIPPhone(
-        "127.0.0.1",
-        5060,
+        SERVER_HOST,
+        UDP_PORT,
         "nopass",
         CredentialsManager(),
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
     )
     assert phone.get_status() == PhoneStatus.INACTIVE
@@ -77,11 +81,11 @@ def test_pass():
     cm = CredentialsManager()
     cm.add("pass", "Testing123!")
     phone = VoIPPhone(
-        "127.0.0.1",
-        5060,
+        SERVER_HOST,
+        UDP_PORT,
         "pass",
         cm,
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
     )
     assert phone.get_status() == PhoneStatus.INACTIVE
@@ -100,11 +104,11 @@ def test_pass():
 @pytest.mark.skipif(TEST_CONDITION, reason=REASON)
 def test_tcp_nopass():
     phone = VoIPPhone(
-        "127.0.0.1",
-        5061,
+        SERVER_HOST,
+        TCP_PORT,
         "nopass",
         CredentialsManager(),
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
         transport_mode=TransportMode.TCP,
     )
@@ -126,11 +130,11 @@ def test_tcp_pass():
     cm = CredentialsManager()
     cm.add("pass", "Testing123!")
     phone = VoIPPhone(
-        "127.0.0.1",
-        5061,
+        SERVER_HOST,
+        TCP_PORT,
         "pass",
         cm,
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
         transport_mode=TransportMode.TCP,
     )
@@ -150,11 +154,11 @@ def test_tcp_pass():
 @pytest.mark.skipif(TEST_CONDITION, reason=REASON)
 def test_tls_nopass():
     phone = VoIPPhone(
-        "127.0.0.1",
-        5062,
+        SERVER_HOST,
+        TLS_PORT,
         "nopass",
         CredentialsManager(),
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
         transport_mode=TransportMode.TLS,
         cert_file="certs/cert.crt",
@@ -179,11 +183,11 @@ def test_tls_pass():
     cm = CredentialsManager()
     cm.add("pass", "Testing123!")
     phone = VoIPPhone(
-        "127.0.0.1",
-        5062,
+        SERVER_HOST,
+        TLS_PORT,
         "pass",
         cm,
-        bind_ip="127.0.0.1",
+        hostname="host.docker.internal",
         bind_port=5059,
         transport_mode=TransportMode.TLS,
         cert_file="certs/cert.crt",

From d74d80dc63a6646d96861bdb330e3467090fe921 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Sun, 15 Oct 2023 18:46:25 -0500
Subject: [PATCH 06/13] [FIX] Fixed typing issue impacting Python 3.8 [FIX]
 Fixed flake8 error

---
 pyVoIP/SIP/client.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index 366ab74..f4435f6 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -307,7 +307,7 @@ def __gen_contact(
         password: Optional[str] = None,
         port=5060,
         uriparams: Optional[str] = None,
-        params: list[str] = [],
+        params: List[str] = [],
     ) -> str:
         uri = self.__gen_uri(method, user, host, password, port, uriparams)
         uri = f"<{uri}>"
@@ -723,7 +723,6 @@ def gen_answer(
             + f"{request.headers['CSeq']['method']}\r\n"
         )
         method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
-        trans_mode = str(self.transport_mode)
         regRequest += self.__gen_contact(
             method,
             self.user,

From 5f01dc9ef82dc0619bada41732d3be2031f148b3 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Sun, 15 Oct 2023 18:49:57 -0500
Subject: [PATCH 07/13] [CHANGE] Turned back on functionality tests.

---
 .github/workflows/pytest.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 85c40ec..97c2125 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -35,5 +35,4 @@ jobs:
           docker build docker -t pyvoip/tests
           docker run --add-host host.docker.internal:host-gateway -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
       - name: pytest
-        # run: pytest --check-func
-        run: pytest
+        run: pytest --check-func

From e26fa04268986b7299f126bd63d3a3cfd6528dd8 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Sun, 29 Oct 2023 13:31:40 -0500
Subject: [PATCH 08/13] [CHANGE] Ran black on docs.

---
 docs/conf.py | 22 ++++++++++------------
 1 file changed, 10 insertions(+), 12 deletions(-)

diff --git a/docs/conf.py b/docs/conf.py
index 69e281b..dbbb176 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -19,26 +19,24 @@
 
 # -- Project information -----------------------------------------------------
 
-project = 'pyVoIP'
-copyright = '2023, Tayler Porter'
-author = 'Tayler J Porter'
+project = "pyVoIP"
+copyright = "2023, Tayler Porter"
+author = "Tayler J Porter"
 
 # The full version, including alpha/beta/rc tags
-release = '2.0.0'
+release = "2.0.0"
 
-master_doc = 'index'
+master_doc = "index"
 
 # -- General configuration ---------------------------------------------------
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
-extensions = [ 
-  "sphinx_rtd_theme"
-]
+extensions = ["sphinx_rtd_theme"]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
@@ -51,11 +49,11 @@
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_rtd_theme"
 
-#pygments_style = 'sphinx'
+# pygments_style = 'sphinx'
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]

From 0b3f1b395acda845bdf13e50d822b6dce6e30797 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Tue, 31 Oct 2023 00:16:21 -0500
Subject: [PATCH 09/13] [ADD] Added basic Network Address Translation. [ADD]
 Added receiver thread to VoIPCall for handling future requests. [ADD] Added
 SIP State DB location global variable for debugging. [ADD] Added AddressType
 enum to support NAT-based determine_tags functions. [ADD] Added check_host
 function in NAT to check the AddressType of a host. [ADD] Added filtering to
 func test if on Windows. [ADD] Added helper variables to ensure func tests
 run correctly on linux. [CHANGE] Cleaned up response checks in SIP client.
 [CHANGE] Broke up VoIPCall init into multiple functions. [CHANGE] Changed
 determine_tags to use NAT. [CHANGE] Changed SIPMessage to always raise
 SIPParseError upon any error. [CHANGE] Added pprint option to
 get_database_dump. [FIX] Fixed port not showing when not 5060 in Contact
 header. [FIX] Fixed invites not working for UDP. [FIX] Fixed missing NSD
 update in VoIPPhone stop. [FIX] Fixed messages not sending when they are a
 UDP response. [FIX] Fixed UDP socket recv function not receiving messages
 missing tags. [FIX] Fixed SQL connection issues.

---
 pyVoIP/SIP/client.py        |  36 ++++--
 pyVoIP/SIP/message.py       |   7 +-
 pyVoIP/VoIP/call.py         | 228 +++++++++++++++++++++---------------
 pyVoIP/VoIP/phone.py        |   1 +
 pyVoIP/__init__.py          |  11 ++
 pyVoIP/networking/nat.py    |  28 ++++-
 pyVoIP/sock/sock.py         |  98 +++++++++-------
 tests/test_functionality.py |  44 ++++++-
 8 files changed, 300 insertions(+), 153 deletions(-)

diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index f4435f6..1229538 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -30,6 +30,13 @@
 debug = pyVoIP.debug
 
 
+UNAUTORIZED_RESPONSE_CODES = [
+    SIPStatus.UNAUTHORIZED,
+    SIPStatus.PROXY_AUTHENTICATION_REQUIRED,
+]
+INVITE_OK_RESPONSE_CODES = [SIPStatus.TRYING, SIPStatus.RINGING, SIPStatus.OK]
+
+
 class SIPClient:
     def __init__(
         self,
@@ -185,6 +192,7 @@ def start(self) -> None:
             self.transport_mode,
             self.bind_ip,
             self.bind_port,
+            self.nat,
             self.cert_file,
             self.key_file,
             self.key_password,
@@ -198,9 +206,11 @@ def start(self) -> None:
         self.s.start()
         # TODO: Check if we need to register with a server or proxy.
         self.register()
+        """
         t = Timer(1, self.recv)
         t.name = "SIP Receive"
         t.start()
+        """
 
     def stop(self) -> None:
         self.NSD = False
@@ -533,6 +543,7 @@ def gen_first_request(self, deregister=False) -> str:
             method,
             self.user,
             self.nat.get_host(self.server),
+            port=self.bind_port,
             uriparams=f";transport={trans_mode}",
             params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
         )
@@ -568,6 +579,7 @@ def gen_subscribe(self, response: SIPMessage) -> str:
             method,
             self.user,
             self.nat.get_host(self.server),
+            port=self.bind_port,
             uriparams=f";transport={trans_mode}",
             params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
         )
@@ -602,6 +614,7 @@ def gen_register(self, request: SIPMessage, deregister=False) -> str:
             method,
             self.user,
             self.nat.get_host(self.server),
+            port=self.bind_port,
             uriparams=f";transport={trans_mode}",
             params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
         )
@@ -727,6 +740,7 @@ def gen_answer(
             method,
             self.user,
             self.nat.get_host(self.server),
+            port=self.bind_port,
         )
         # TODO: Add Supported
         regRequest += self.__gen_user_agent()
@@ -787,6 +801,7 @@ def gen_invite(
             method,
             self.user,
             self.nat.get_host(self.server),
+            port=self.bind_port,
         )
         invRequest += self.__gen_from_to(
             "To", number, self.server, port=self.port
@@ -831,6 +846,7 @@ def _gen_bye_cancel(self, request: SIPMessage, cmd: str) -> str:
             method,
             self.user,
             self.nat.get_host(self.server),
+            port=self.bind_port,
         )
         byeRequest += self.__gen_user_agent()
         byeRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
@@ -905,10 +921,8 @@ def invite(
         response = SIPMessage(conn.recv(8192))
 
         while (
-            response.status != SIPStatus(401)
-            and response.status != SIPStatus(407)
-            and response.status != SIPStatus(100)
-            and response.status != SIPStatus(180)
+            response.status
+            not in UNAUTORIZED_RESPONSE_CODES + INVITE_OK_RESPONSE_CODES
         ) or response.headers["Call-ID"] != call_id:
             if not self.NSD:
                 break
@@ -918,14 +932,16 @@ def invite(
 
         debug(f"Received Response: {response.summary()}")
 
-        if response.status == SIPStatus(100) or response.status == SIPStatus(
-            180
-        ):
-            debug("Invite status OK")
-            return SIPMessage(invite.encode("utf8")), call_id, sess_id
+        if response.status in INVITE_OK_RESPONSE_CODES:
+            debug("Invite Accepted")
+            if response.status is SIPStatus.OK:
+                return response, call_id, sess_id, conn
+            return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn
+        debug("Invite Requires Authorization")
         ack = self.gen_ack(response)
         conn.send(ack)
         debug("Acknowledged")
+        conn.close()  # End of Dialog
         auth = self.gen_authorization(response)
 
         invite = self.gen_invite(
@@ -935,7 +951,7 @@ def invite(
             "\r\nContent-Length", f"\r\n{auth}Content-Length"
         )
 
-        conn.send(invite)
+        conn = self.sendto(invite)
 
         return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn
 
diff --git a/pyVoIP/SIP/message.py b/pyVoIP/SIP/message.py
index 5ab7368..e14bafe 100644
--- a/pyVoIP/SIP/message.py
+++ b/pyVoIP/SIP/message.py
@@ -313,7 +313,12 @@ def __init__(self, data: bytes):
             "v": "Via",
         }
 
-        self.parse(data)
+        try:
+            self.parse(data)
+        except Exception as e:
+            if type(e) is not SIPParseError:
+                raise SIPParseError(e)
+            raise
 
     def summary(self) -> str:
         data = ""
diff --git a/pyVoIP/VoIP/call.py b/pyVoIP/VoIP/call.py
index be4975f..d250555 100644
--- a/pyVoIP/VoIP/call.py
+++ b/pyVoIP/VoIP/call.py
@@ -1,7 +1,9 @@
 from enum import Enum
 from pyVoIP import RTP, SIP
+from pyVoIP.SIP.error import SIPParseError
+from pyVoIP.SIP.message import SIPMessage, SIPMessageType, SIPStatus
 from pyVoIP.VoIP.error import InvalidStateError
-from threading import Lock
+from threading import Lock, Timer
 from typing import Any, Dict, List, Optional, TYPE_CHECKING
 import audioop
 import io
@@ -37,7 +39,7 @@ def __init__(
         self,
         phone: "VoIPPhone",
         callstate: CallState,
-        request: SIP.SIPMessage,
+        request: SIPMessage,
         session_id: int,
         bind_ip: str,
         conn: "VoIPConnection",
@@ -51,6 +53,7 @@ def __init__(
         self.call_id = request.headers["Call-ID"]
         self.session_id = str(session_id)
         self.bind_ip = bind_ip
+        self.conn = conn
         self.rtp_port_high = self.phone.rtp_port_high
         self.rtp_port_low = self.phone.rtp_port_low
         self.sendmode = sendmode
@@ -71,103 +74,134 @@ def __init__(
         self.assignedPorts: Any = {}
 
         if callstate == CallState.RINGING:
-            audio = []
-            video = []
-            for x in self.request.body["c"]:
-                self.connections += x["address_count"]
-            for x in self.request.body["m"]:
-                if x["type"] == "audio":
-                    self.audioPorts += x["port_count"]
-                    audio.append(x)
-                elif x["type"] == "video":
-                    self.videoPorts += x["port_count"]
-                    video.append(x)
-                else:
-                    warnings.warn(
-                        f"Unknown media description: {x['type']}", stacklevel=2
-                    )
-
-            # Ports Adjusted is used in case of multiple m tags.
-            if len(audio) > 0:
-                audioPortsAdj = self.audioPorts / len(audio)
+            self.init_incoming_call(request)
+        elif callstate == CallState.DIALING:
+            self.init_outgoing_call(ms)
+
+        t = Timer(0, self.receiver)
+        t.name = f"Call {self.call_id} Receiver"
+        t.start()
+
+    def receiver(self):
+        """Receive and handle incoming messages"""
+        while self.state is not CallState.ENDED and self.phone.NSD:
+            data = self.conn.recv(8192)
+            if data is None:
+                continue
+            try:
+                message = SIPMessage(data)
+            except SIPParseError:
+                continue
+            if message.type is SIPMessageType.RESPONSE:
+                if message.status is SIPStatus.OK:
+                    if self.state in [
+                        CallState.DIALING,
+                        CallState.RINGING,
+                        CallState.PROGRESS,
+                    ]:
+                        self.answered(message)
+                elif message.status == SIPStatus.NOT_FOUND:
+                    pass
             else:
-                audioPortsAdj = 0
-            if len(video) > 0:
-                videoPortsAdj = self.videoPorts / len(video)
+                if message.method == "BYE":
+                    self.bye(message)
+
+    def init_outgoing_call(self, ms: Optional[Dict[int, RTP.PayloadType]]):
+        if ms is None:
+            raise RuntimeError(
+                "Media assignments are required when " + "initiating a call"
+            )
+        self.ms = ms
+        for m in self.ms:
+            self.port = m
+            self.assignedPorts[m] = self.ms[m]
+
+    def init_incoming_call(self, request: SIP.SIPMessage):
+        audio = []
+        video = []
+        for x in self.request.body["c"]:
+            self.connections += x["address_count"]
+        for x in self.request.body["m"]:
+            if x["type"] == "audio":
+                self.audioPorts += x["port_count"]
+                audio.append(x)
+            elif x["type"] == "video":
+                self.videoPorts += x["port_count"]
+                video.append(x)
             else:
-                videoPortsAdj = 0
+                warnings.warn(
+                    f"Unknown media description: {x['type']}", stacklevel=2
+                )
 
-            if not (
-                (audioPortsAdj == self.connections or self.audioPorts == 0)
-                and (videoPortsAdj == self.connections or self.videoPorts == 0)
-            ):
-                # TODO: Throw error to PBX in this case
-                warnings.warn("Unable to assign ports for RTP.", stacklevel=2)
-                return
-
-            for i in request.body["m"]:
-                assoc = {}
-                e = False
-                for x in i["methods"]:
+        # Ports Adjusted is used in case of multiple m tags.
+        if len(audio) > 0:
+            audioPortsAdj = self.audioPorts / len(audio)
+        else:
+            audioPortsAdj = 0
+        if len(video) > 0:
+            videoPortsAdj = self.videoPorts / len(video)
+        else:
+            videoPortsAdj = 0
+
+        if not (
+            (audioPortsAdj == self.connections or self.audioPorts == 0)
+            and (videoPortsAdj == self.connections or self.videoPorts == 0)
+        ):
+            # TODO: Throw error to PBX in this case
+            warnings.warn("Unable to assign ports for RTP.", stacklevel=2)
+            return
+
+        for i in request.body["m"]:
+            assoc = {}
+            e = False
+            for x in i["methods"]:
+                try:
+                    p = RTP.PayloadType(int(x))
+                    assoc[int(x)] = p
+                except ValueError:
                     try:
-                        p = RTP.PayloadType(int(x))
+                        p = RTP.PayloadType(
+                            i["attributes"][x]["rtpmap"]["name"]
+                        )
                         assoc[int(x)] = p
                     except ValueError:
-                        try:
-                            p = RTP.PayloadType(
-                                i["attributes"][x]["rtpmap"]["name"]
-                            )
-                            assoc[int(x)] = p
-                        except ValueError:
-                            # Sometimes rtpmap raise a KeyError because fmtp
-                            # is set instate
-                            pt = i["attributes"][x]["rtpmap"]["name"]
-                            warnings.warn(
-                                f"RTP Payload type {pt} not found.",
-                                stacklevel=20,
-                            )
-                            # Resets the warning filter so this warning will
-                            # come up again if it happens.  However, this
-                            # also resets all other warnings.
-                            warnings.simplefilter("default")
-                            p = RTP.PayloadType("UNKNOWN")
-                            assoc[int(x)] = p
-                        except KeyError:
-                            # fix issue 42
-                            # When rtpmap is not found, also set the found
-                            # element to UNKNOWN
-                            warnings.warn(
-                                f"RTP KeyError {x} not found.", stacklevel=20
-                            )
-                            p = RTP.PayloadType("UNKNOWN")
-                            assoc[int(x)] = p
-
-                if e:
-                    raise RTP.RTPParseError(
-                        f"RTP Payload type {pt} not found."
-                    )
-
-                # Make sure codecs are compatible.
-                codecs = {}
-                for m in assoc:
-                    if assoc[m] in pyVoIP.RTPCompatibleCodecs:
-                        codecs[m] = assoc[m]
-                # TODO: If no codecs are compatible then send error to PBX.
-
-                port = self.phone.request_port()
-                self.create_rtp_clients(
-                    codecs, self.bind_ip, port, request, i["port"]
-                )
-        elif callstate == CallState.DIALING:
-            if ms is None:
-                raise RuntimeError(
-                    "Media assignments are required when "
-                    + "initiating a call"
-                )
-            self.ms = ms
-            for m in self.ms:
-                self.port = m
-                self.assignedPorts[m] = self.ms[m]
+                        # Sometimes rtpmap raise a KeyError because fmtp
+                        # is set instate
+                        pt = i["attributes"][x]["rtpmap"]["name"]
+                        warnings.warn(
+                            f"RTP Payload type {pt} not found.",
+                            stacklevel=20,
+                        )
+                        # Resets the warning filter so this warning will
+                        # come up again if it happens.  However, this
+                        # also resets all other warnings.
+                        warnings.simplefilter("default")
+                        p = RTP.PayloadType("UNKNOWN")
+                        assoc[int(x)] = p
+                    except KeyError:
+                        # fix issue 42
+                        # When rtpmap is not found, also set the found
+                        # element to UNKNOWN
+                        warnings.warn(
+                            f"RTP KeyError {x} not found.", stacklevel=20
+                        )
+                        p = RTP.PayloadType("UNKNOWN")
+                        assoc[int(x)] = p
+
+            if e:
+                raise RTP.RTPParseError(f"RTP Payload type {pt} not found.")
+
+            # Make sure codecs are compatible.
+            codecs = {}
+            for m in assoc:
+                if assoc[m] in pyVoIP.RTPCompatibleCodecs:
+                    codecs[m] = assoc[m]
+            # TODO: If no codecs are compatible then send error to PBX.
+
+            port = self.phone.request_port()
+            self.create_rtp_clients(
+                codecs, self.bind_ip, port, request, i["port"]
+            )
 
     def create_rtp_clients(
         self,
@@ -280,6 +314,8 @@ def answered(self, request: SIP.SIPMessage) -> None:
         elif self.state != CallState.PROGRESS:
             return
         self.state = CallState.ANSWERED
+        ack = self.phone.sip.gen_ack(request)
+        self.conn.send(ack)
 
     def progress(self, request: SIP.SIPMessage) -> None:
         if self.state != CallState.DIALING:
@@ -377,7 +413,7 @@ def cancel(self) -> None:
         self.sip.cancel(self.request)
         self.state = CallState.CANCELING
 
-    def bye(self) -> None:
+    def bye(self, request: SIPMessage) -> None:
         if (
             self.state == CallState.ANSWERED
             or self.state == CallState.PROGRESS
@@ -385,7 +421,9 @@ def bye(self) -> None:
         ):
             for x in self.RTPClients:
                 x.stop()
-            self.state = CallState.ENDED
+        self.state = CallState.ENDED
+        ok = self.phone.sip.gen_ok(request)
+        self.conn.send(ok)
         if self.request.headers["Call-ID"] in self.phone.calls:
             del self.phone.calls[self.request.headers["Call-ID"]]
 
diff --git a/pyVoIP/VoIP/phone.py b/pyVoIP/VoIP/phone.py
index 9f5bde3..9833bdc 100644
--- a/pyVoIP/VoIP/phone.py
+++ b/pyVoIP/VoIP/phone.py
@@ -311,6 +311,7 @@ def stop(self) -> None:
             except InvalidStateError:
                 pass
         self.sip.stop()
+        self.NSD = False
         self._status = PhoneStatus.INACTIVE
 
     def call(
diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py
index 858b3d4..e62e2f7 100644
--- a/pyVoIP/__init__.py
+++ b/pyVoIP/__init__.py
@@ -50,6 +50,17 @@
 # https://docs.python.org/3/library/ssl.html#ssl.SSLContext.verify_mode
 TLS_VERIFY_MODE = ssl.CERT_REQUIRED
 
+"""
+DO NOT CHANGE IN PRODUCTION.
+
+This variable allows you to save the SIP message state database to a file
+instead of storing it in memory which is the default.  This is useful for
+debugging, however pyVoIP does not delete the database afterwards which will
+cause an Exception upon restarting pyVoIP.  For this reason, we recommend you
+do not change this variable in production.
+"""
+SIP_STATE_DB_LOCATION = ":memory:"
+
 
 def set_tls_security(verify_mode: ssl.VerifyMode) -> None:
     """
diff --git a/pyVoIP/networking/nat.py b/pyVoIP/networking/nat.py
index bea9de9..e07470f 100644
--- a/pyVoIP/networking/nat.py
+++ b/pyVoIP/networking/nat.py
@@ -1,3 +1,4 @@
+from enum import Enum
 from typing import Optional
 import ipaddress
 import socket
@@ -7,6 +8,13 @@ class NATError(Exception):
     pass
 
 
+class AddressType(Enum):
+    """Used for determining remote or local tags in SIP messages"""
+
+    REMOTE = 0
+    LOCAL = 1
+
+
 class NAT:
     def __init__(
         self,
@@ -26,7 +34,7 @@ def get_host(self, host: str):
             ip = ipaddress.ip_address(host)
         except ValueError:
             try:
-                ip = socket.gethostbyname(host)
+                ip = ipaddress.ip_address(socket.gethostbyname(host))
             except socket.gaierror:
                 raise NATError(f"Unable to resolve hostname {host}")
 
@@ -39,3 +47,21 @@ def get_host(self, host: str):
                 "No remote hostname specified, "
                 + "cannot provide a return path for remote hosts."
             )
+
+    def check_host(self, host: str) -> AddressType:
+        """Determine if a host is a remote computer or not."""
+        if host in [self.remote_hostname, self.hostname]:
+            return AddressType.LOCAL
+        try:
+            ip = ipaddress.ip_address(host)
+            if ip == self.bind_ip:
+                return AddressType.LOCAL
+            return AddressType.REMOTE
+        except ValueError:
+            try:
+                ip = ipaddress.ip_address(socket.gethostbyname(host))
+                if ip == self.bind_ip:
+                    return AddressType.LOCAL
+                return AddressType.REMOTE
+            except socket.gaierror:
+                return AddressType.REMOTE
diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index 958670c..8937c90 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -1,9 +1,13 @@
 from typing import List, Optional, Tuple, Union
+from pyVoIP import SIP_STATE_DB_LOCATION
 from pyVoIP.types import KEY_PASSWORD, SOCKETS
 from pyVoIP.SIP import SIPMessage, SIPMessageType
+from pyVoIP.SIP.error import SIPParseError
+from pyVoIP.networking.nat import NAT, AddressType
 from pyVoIP.sock.transport import TransportMode
 import json
 import math
+import pprint
 import pyVoIP
 import socket
 import sqlite3
@@ -48,12 +52,15 @@ def __init__(
     def send(self, data: Union[bytes, str]) -> None:
         if type(data) is str:
             data = data.encode("utf8")
-        msg = SIPMessage(data)
+        try:
+            msg = SIPMessage(data)
+        except SIPParseError:
+            return
         if not self.conn:  # If UDP
             if msg.type == SIPMessageType.REQUEST:
                 addr = (msg.to["host"], msg.to["port"])
             else:
-                addr = msg.headers["Via"][0]
+                addr = msg.headers["Via"][0]["address"]
             self.sock.s.sendto(data, addr)
         else:
             self.conn.send(data)
@@ -71,7 +78,7 @@ def __find_remote_tag(self) -> None:
         )
         rows = result.fetchall()
         if rows:
-            print(f"Found remote: {rows[0][0]}")
+            # print(f"Found remote: {rows[0][0]}")
             self.remote_tag = rows[0][0]
 
     def recv(self, nbytes: int, timeout=0) -> bytes:
@@ -83,7 +90,7 @@ def recv(self, nbytes: int, timeout=0) -> bytes:
                 data = self.conn.recv(nbytes)
                 try:
                     msg = SIPMessage(data)
-                except Exception as e:
+                except SIPParseError as e:
                     br = self.sock.gen_bad_request(
                         connection=self, error=e, received=data
                     )
@@ -101,20 +108,34 @@ def recv(self, nbytes: int, timeout=0) -> bytes:
                 conn.row_factory = sqlite3.Row
                 sql = (
                     'SELECT * FROM "msgs" WHERE "call_id"=? AND "local_tag"=?'
-                    + (""" AND "remote_tag" = ?""" if self.remote_tag else "")
                 )
+                if self.remote_tag:
+                    sql += (
+                        ' UNION SELECT * FROM "msgs" WHERE "call_id"=? AND '
+                        + '"local_tag"=? AND "remote_tag"=?'
+                    )
                 bindings = (
-                    (self.call_id, self.local_tag, self.remote_tag)
+                    (
+                        self.call_id,
+                        self.local_tag,
+                        self.call_id,
+                        self.local_tag,
+                        self.remote_tag,
+                    )
                     if self.remote_tag
                     else (self.call_id, self.local_tag)
                 )
                 result = conn.execute(sql, bindings)
                 row = result.fetchone()
                 if not row:
+                    conn.close()
                     continue
-                conn.execute('DELETE FROM "msgs" WHERE "id" = ?', (row["id"],))
                 try:
                     self.sock.buffer.commit()
+                    conn.execute(
+                        'DELETE FROM "msgs" WHERE "id" = ?', (row["id"],)
+                    )
+                    self.sock.buffer.commit()
                 except sqlite3.OperationalError:
                     pass
                 conn.close()
@@ -135,6 +156,7 @@ def __init__(
         mode: TransportMode,
         bind_ip: str,
         bind_port: int,
+        nat: NAT,
         cert_file: Optional[str] = None,
         key_file: Optional[str] = None,
         key_password: KEY_PASSWORD = None,
@@ -148,6 +170,7 @@ def __init__(
         self.s = socket.socket(socket.AF_INET, mode.socket_type)
         self.bind_ip: str = bind_ip
         self.bind_port: int = bind_port
+        self.nat = nat
         self.server_context: Optional[ssl.SSLContext] = None
         if mode.tls_mode:
             self.server_context = ssl.SSLContext(
@@ -160,7 +183,9 @@ def __init__(
                 )
             self.s = self.server_context.wrap_socket(self.s, server_side=True)
 
-        self.buffer = sqlite3.connect(":memory:", check_same_thread=False)
+        self.buffer = sqlite3.connect(
+            SIP_STATE_DB_LOCATION, check_same_thread=False
+        )
         """
         RFC 3261 Section 12, Paragraph 2 states:
         "A dialog is identified at each UA with a dialog ID, which consists
@@ -319,9 +344,9 @@ def deregister_connection(self, connection: VoIPConnection) -> None:
         if self.mode is not TransportMode.UDP:
             return
         self.conns_lock.acquire()
-        print(f"Deregistering {connection}")
-        print(f"{self.conns=}")
-        print(self.get_database_dump())
+        debug(f"Deregistering {connection}")
+        debug(f"{self.conns=}")
+        debug(self.get_database_dump())
         try:
             conn = self.buffer.cursor()
             result = conn.execute(
@@ -348,57 +373,38 @@ def deregister_connection(self, connection: VoIPConnection) -> None:
             pass
         finally:
             conn.close()
-            print("Deregistered")
-            print(f"{self.conns=}")
-            print(self.get_database_dump())
             self.conns_lock.release()
 
-    def get_database_dump(self) -> str:
+    def get_database_dump(self, pretty=False) -> str:
         conn = self.buffer.cursor()
         ret = ""
         try:
             result = conn.execute('SELECT * FROM "listening";')
-            ret += "listening: " + json.dumps(result.fetchall()) + "\n\n"
+            result1 = result.fetchall()
             result = conn.execute('SELECT * FROM "msgs";')
-            ret += "msgs: " + json.dumps(result.fetchall()) + "\n\n"
+            result2 = result.fetchall()
         finally:
             conn.close()
-            return ret
+        if pretty:
+            ret += "listening: " + pprint.pformat(result1) + "\n\n"
+            ret += "msgs: " + pprint.pformat(result2) + "\n\n"
+        else:
+            ret += "listening: " + json.dumps(result1) + "\n\n"
+            ret += "msgs: " + json.dumps(result2) + "\n\n"
+        return ret
 
     def determine_tags(self, message: SIPMessage) -> Tuple[str, str]:
         """
         Return local_tag, remote_tag
         """
-        # TODO: Eventually NAT will be supported for people who don't have a SIP ALG
-        # We will need to take that into account when determining the remote tag.
 
         to_header = message.headers["To"]
         from_header = message.headers["From"]
         to_host = to_header["host"]
-        to_port = to_header["port"]
         to_tag = to_header["tag"] if to_header["tag"] else None
-        from_host = from_header["host"]
-        from_port = from_header["port"]
         from_tag = from_header["tag"] if from_header["tag"] else None
 
-        if to_host == self.bind_ip and to_port == self.bind_port and to_tag:
-            return to_tag, from_tag
-        elif from_host == self.bind_ip and from_port == self.bind_port:
-            return from_tag, to_tag
-        # If there is not an exact match, see if the host at least matches.
-        # (But not if the hosts are the same) as asterisk likes to strip
-        # ports.
-        elif to_host != from_host:
-            if to_host == self.bind_ip:
-                return to_tag, from_tag
-            elif from_host == self.bind_ip:
-                return from_tag, to_tag
-        # If there is still not a match, guess the to or from tag based
-        # on if the message. Requests except ACK likely have us as To,
-        # for everthing else we're likely the From
-        elif (
-            message.type == SIPMessageType.REQUEST and message.method != "ACK"
-        ):
+        if self.nat.check_host(to_host) is AddressType.LOCAL:
             return to_tag, from_tag
         return from_tag, to_tag
 
@@ -421,7 +427,10 @@ def run(self) -> None:
                     data = self.s.recv(8192)
                 except OSError:
                     continue
-                message = SIPMessage(data)
+                try:
+                    message = SIPMessage(data)
+                except SIPParseError:
+                    continue
                 debug("\n\nReceived UDP Message:")
                 debug(message.summary())
             else:
@@ -431,7 +440,10 @@ def run(self) -> None:
                     continue
                 debug(f"Received new {self.mode} connection from {addr}.")
                 data = conn.recv(8192)
-                message = SIPMessage(data)
+                try:
+                    message = SIPMessage(data)
+                except SIPParseError:
+                    continue
                 debug("\n\nReceived SIP Message:")
                 debug(message.summary())
 
diff --git a/tests/test_functionality.py b/tests/test_functionality.py
index 7e6f55b..22cebe7 100644
--- a/tests/test_functionality.py
+++ b/tests/test_functionality.py
@@ -2,22 +2,42 @@
 from pyVoIP.VoIP.call import CallState
 from pyVoIP.VoIP.phone import PhoneStatus, VoIPPhone
 from pyVoIP.sock.transport import TransportMode
+import json
+import os
 import pytest
 import pyVoIP
 import ssl
+import subprocess
 import sys
 import time
 
+
+IS_WINDOWS = True if os.name == "nt" else False
 TEST_CONDITION = (
     "--check-functionality" not in sys.argv and "--check-func" not in sys.argv
 )
+
+if not TEST_CONDITION and not IS_WINDOWS:
+    obj = json.loads(
+        subprocess.check_output(["docker", "network", "inspect", "bridge"])
+    )
+    DOCKER_GATEWAY = obj[0]["IPAM"]["Config"][0]["Gateway"]
+    CONTAINER_ID = list(obj[0]["Containers"].keys())[0]
+    CONTAINER_IP = obj[0]["Containers"][CONTAINER_ID]["IPv4Address"].split(
+        "/"
+    )[0]
+
 REASON = "Not checking functionality"
+NT_REASON = "Test always fails on Windows"
 pyVoIP.set_tls_security(ssl.CERT_NONE)
-SERVER_HOST = "127.0.0.1"
+SERVER_HOST = "127.0.0.1" if IS_WINDOWS else CONTAINER_IP
+BIND_IP = "0.0.0.0" if IS_WINDOWS else DOCKER_GATEWAY
 UDP_PORT = 5060
 TCP_PORT = 5061
 TLS_PORT = 5062
 
+CALL_TIMEOUT = 2  # 2 seconds to answer.
+
 
 @pytest.fixture
 def phone():
@@ -29,6 +49,7 @@ def phone():
         "pass",
         cm,
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
     )
     phone.start()
@@ -44,6 +65,7 @@ def nopass_phone():
         "nopass",
         CredentialsManager(),
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
     )
     phone.start()
@@ -61,6 +83,7 @@ def test_nopass():
         "nopass",
         CredentialsManager(),
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
     )
     assert phone.get_status() == PhoneStatus.INACTIVE
@@ -86,6 +109,7 @@ def test_pass():
         "pass",
         cm,
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
     )
     assert phone.get_status() == PhoneStatus.INACTIVE
@@ -109,6 +133,7 @@ def test_tcp_nopass():
         "nopass",
         CredentialsManager(),
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
         transport_mode=TransportMode.TCP,
     )
@@ -135,6 +160,7 @@ def test_tcp_pass():
         "pass",
         cm,
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
         transport_mode=TransportMode.TCP,
     )
@@ -159,6 +185,7 @@ def test_tls_nopass():
         "nopass",
         CredentialsManager(),
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
         transport_mode=TransportMode.TLS,
         cert_file="certs/cert.crt",
@@ -188,6 +215,7 @@ def test_tls_pass():
         "pass",
         cm,
         hostname="host.docker.internal",
+        bind_ip=BIND_IP,
         bind_port=5059,
         transport_mode=TransportMode.TLS,
         cert_file="certs/cert.crt",
@@ -205,27 +233,33 @@ def test_tls_pass():
     assert phone.get_status() == PhoneStatus.INACTIVE
 
 
-@pytest.mark.skip
 @pytest.mark.udp
 @pytest.mark.calling
 @pytest.mark.skipif(TEST_CONDITION, reason=REASON)
+@pytest.mark.skipif(IS_WINDOWS, reason=NT_REASON)
 def test_make_call(phone):
     call = phone.call("answerme")
+    start = time.time()
     while call.state == CallState.DIALING:
         time.sleep(0.1)
+        if start + CALL_TIMEOUT < time.time():
+            raise TimeoutError("Call was not answered before the timeout.")
     assert call.state == CallState.ANSWERED
     call.hangup()
     assert call.state == CallState.ENDED
 
 
-@pytest.mark.skip
 @pytest.mark.udp
 @pytest.mark.calling
 @pytest.mark.skipif(TEST_CONDITION, reason=REASON)
+@pytest.mark.skipif(IS_WINDOWS, reason=NT_REASON)
 def test_make_nopass_call(nopass_phone):
     call = nopass_phone.call("answerme")
+    start = time.time()
     while call.state == CallState.DIALING:
         time.sleep(0.1)
+        if start + CALL_TIMEOUT < time.time():
+            raise TimeoutError("Call was not answered before the timeout.")
     assert call.state == CallState.ANSWERED
     call.hangup()
     assert call.state == CallState.ENDED
@@ -235,10 +269,14 @@ def test_make_nopass_call(nopass_phone):
 @pytest.mark.udp
 @pytest.mark.calling
 @pytest.mark.skipif(TEST_CONDITION, reason=REASON)
+@pytest.mark.skipif(IS_WINDOWS, reason=NT_REASON)
 def test_remote_hangup(phone):
     call = phone.call("answerme")
+    start = time.time()
     while call.state == CallState.DIALING:
         time.sleep(0.1)
+        if start + CALL_TIMEOUT < time.time():
+            raise TimeoutError("Call was not answered before the timeout.")
     assert call.state == CallState.ANSWERED
     time.sleep(5)
     assert call.state == CallState.ENDED

From 9fed4a0552346ad1da86a95dfd2f9f2619143592 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Sat, 11 Nov 2023 22:17:00 -0600
Subject: [PATCH 10/13] [CHANGE] Broke up VoIPSocket.run into TCP/TLS and UDP
 run functions. [FIX] Fixed multiple SQL issues. [FIX] Fixed line length.
 [FIX] Fixed __get_connection.

---
 pyVoIP/sock/sock.py | 173 +++++++++++++++++++-------------------------
 1 file changed, 75 insertions(+), 98 deletions(-)

diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index 8937c90..d682c78 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -214,7 +214,7 @@ def __init__(
         conn.execute(
             """CREATE TABLE "listening" (
                 "call_id" TEXT NOT NULL,
-                "local_tag" TEXT NOT NULL,
+                "local_tag" TEXT,
                 "remote_tag" TEXT,
                 "connection" INTEGER NOT NULL UNIQUE,
                 PRIMARY KEY("call_id", "local_tag", "remote_tag")
@@ -231,7 +231,8 @@ def __init__(
     def gen_bad_request(
         self, connection=None, message=None, error=None, received=None
     ) -> bytes:
-        body = f"<error><message>{error}</message><received>{received}</received></error>"
+        body = f"<error><message>{error}</message>"
+        body += f"<received>{received}</received></error>"
         bad_request = "SIP/2.0 400 Malformed Request\r\n"
         bad_request += (
             f"Via: SIP/2.0/{self.mode} {self.bind_ip}:{self.bind_port}\r\n"
@@ -250,58 +251,29 @@ def __get_connection(
         local_tag, remote_tag = self.determine_tags(message)
         call_id = message.headers["Call-ID"]
         conn = self.buffer.cursor()
-        result = conn.execute(
-            """SELECT "connection" FROM "listening" WHERE
-                "call_id" = ?
-                AND "local_tag" = ?
-                AND "remote_tag" = ?""",
-            (call_id, local_tag, remote_tag),
-        )
+        sql = 'SELECT "connection" FROM "listening" WHERE "call_id" IS ?'
+        sql += ' AND "local_tag" IS ? AND "remote_tag" IS ?'
+        result = conn.execute(sql, (call_id, local_tag, remote_tag))
         rows = result.fetchall()
         if rows:
             conn.close()
             return self.conns[rows[0][0]]
+        debug("New Connection Started")
         # If we didn't find one lets look for something that doesn't have
-        # a remote tag
-        result = conn.execute(
-            """SELECT "connection" FROM "listening" WHERE
-                "call_id" = ?
-                AND "local_tag" = ?
-                AND "remote_tag" is NULL""",
-            (call_id, local_tag),
-        )
+        # one of the tags
+        sql = 'SELECT "connection" FROM "listening" WHERE "call_id" = ?'
+        sql += ' AND ("local_tag" IS NULL OR "local_tag" = ?)'
+        sql += ' AND ("remote_tag" IS NULL OR "remote_tag" = ?)'
+        result = conn.execute(sql, (call_id, local_tag, remote_tag))
         rows = result.fetchall()
         if rows:
-            conn.execute(
-                """UPDATE "listening" SET
-                    "remote_tag" = ? WHERE
-                    "call_id" = ?
-                    AND "local_tag" = ?
-                    AND "remote_tag" is NULL""",
-                (remote_tag, call_id, local_tag),
-            )
+            if local_tag and remote_tag:
+                sql = 'UPDATE "listening" SET "remote_tag" = ?, '
+                sql += '"local_tag" = ? WHERE "connection" = ?'
+                conn.execute(sql, (remote_tag, local_tag, rows[0][0]))
             conn.close()
             return self.conns[rows[0][0]]
-        # If we still didn't find one, maybe we got the local and remote wrong?
-        result = conn.execute(
-            """SELECT "connection" FROM "listening" WHERE
-                "call_id" = ?
-                AND "local_tag" = ?
-                AND "remote_tag" is NULL""",
-            (call_id, remote_tag),
-        )
-        rows = result.fetchall()
         conn.close()
-        if rows:
-            conn.execute(
-                """UPDATE "listening" SET
-                    "remote_tag" = ? WHERE
-                    "call_id" = ?
-                    AND "local_tag" = ?
-                    AND "remote_tag" is NULL""",
-                (local_tag, call_id, remote_tag),
-            )
-            return self.conns[rows[0][0]]
         return None
 
     def __register_connection(self, connection: VoIPConnection) -> None:
@@ -349,10 +321,11 @@ def deregister_connection(self, connection: VoIPConnection) -> None:
         debug(self.get_database_dump())
         try:
             conn = self.buffer.cursor()
+            sql = 'SELECT "connection" FROM "listening" WHERE "call_id" = ?'
+            sql += ' AND ("local_tag" IS NULL OR "local_tag" = ?)'
+            sql += ' AND ("remote_tag" IS NULL OR "remote_tag" = ?)'
             result = conn.execute(
-                """SELECT "connection" FROM "listening"
-                    WHERE "call_id" = ? AND "local_tag" = ?
-                    AND "remote_tag" = ?""",
+                sql,
                 (
                     connection.call_id,
                     connection.local_tag,
@@ -417,60 +390,64 @@ def bind(self, addr: Tuple[str, int]) -> None:
     def _listen(self, backlog=0) -> None:
         return self.s.listen(backlog)
 
-    def run(self) -> None:
-        self.bind((self.bind_ip, self.bind_port))
-        if self.mode != TransportMode.UDP:
-            self._listen()
+    def _tcp_tls_run(self) -> None:
+        self._listen()
         while not self.SD:
-            if self.mode == TransportMode.UDP:
-                try:
-                    data = self.s.recv(8192)
-                except OSError:
-                    continue
-                try:
-                    message = SIPMessage(data)
-                except SIPParseError:
-                    continue
-                debug("\n\nReceived UDP Message:")
-                debug(message.summary())
-            else:
-                try:
-                    conn, addr = self.s.accept()
-                except OSError:
-                    continue
-                debug(f"Received new {self.mode} connection from {addr}.")
-                data = conn.recv(8192)
-                try:
-                    message = SIPMessage(data)
-                except SIPParseError:
-                    continue
-                debug("\n\nReceived SIP Message:")
-                debug(message.summary())
-
-            if not self.__connection_exists(message):
-                if self.mode == TransportMode.UDP:
-                    self.__register_connection(
-                        VoIPConnection(self, None, message)
-                    )
-                else:
-                    self.__register_connection(
-                        VoIPConnection(self, conn, message)
-                    )
+            try:
+                conn, addr = self.s.accept()
+            except OSError:
+                continue
+            debug(f"Received new {self.mode} connection from {addr}.")
+            data = conn.recv(8192)
+            try:
+                message = SIPMessage(data)
+            except SIPParseError:
+                continue
+            debug("\n\nReceived SIP Message:")
+            debug(message.summary())
+            self._handle_incoming_message(conn, message)
 
-            call_id = message.headers["Call-ID"]
-            local_tag, remote_tag = self.determine_tags(message)
-            raw_message = data.decode("utf8")
-            conn = self.buffer.cursor()
-            conn.execute(
-                "INSERT INTO msgs (call_id, local_tag, remote_tag, msg) "
-                + "VALUES (?, ?, ?, ?)",
-                (call_id, local_tag, remote_tag, raw_message),
-            )
+    def _udp_run(self) -> None:
+        while not self.SD:
             try:
-                self.buffer.commit()
-            except sqlite3.OperationalError:
-                pass
-            conn.close()
+                data = self.s.recv(8192)
+            except OSError:
+                continue
+            try:
+                message = SIPMessage(data)
+            except SIPParseError:
+                continue
+            debug("\n\nReceived UDP Message:")
+            debug(message.summary())
+            self._handle_incoming_message(None, message)
+
+    def _handle_incoming_message(
+        self, conn: Optional[SOCKETS], message: SIPMessage
+    ):
+        if not self.__connection_exists(message):
+            self.__register_connection(VoIPConnection(self, conn, message))
+
+        call_id = message.headers["Call-ID"]
+        local_tag, remote_tag = self.determine_tags(message)
+        raw_message = message.raw.decode("utf8")
+        conn = self.buffer.cursor()
+        conn.execute(
+            "INSERT INTO msgs (call_id, local_tag, remote_tag, msg) "
+            + "VALUES (?, ?, ?, ?)",
+            (call_id, local_tag, remote_tag, raw_message),
+        )
+        try:
+            self.buffer.commit()
+        except sqlite3.OperationalError:
+            pass
+        conn.close()
+
+    def run(self) -> None:
+        self.bind((self.bind_ip, self.bind_port))
+        if self.mode == TransportMode.UDP:
+            self._udp_run()
+        else:
+            self._tcp_tls_run()
 
     def close(self) -> None:
         self.SD = True

From 0902757be42ba40c8c74954ad9b4947091a14d54 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Mon, 27 Nov 2023 07:54:31 -0600
Subject: [PATCH 11/13] [CHANGE] Began migration from SIPClient.recv to
 handle_new_connection [CHANGE] Added VoIPConnection to callback in VoIPPhone
 [CHANGE] Changed _callback_MSG_Invite to use VoIPConnection instead of
 creating          a new dialog [CHANGE] Changed _create_call to be PEP8
 compliant [CHANGE] Moved TCP/TLS recv and UDP recv code to different
 functions for better          readability and maintainability [FIX] Fixed
 errors if calling RTP.stop when not started [FIX] Fixed issue with code not
 responding to SIP_STATE_DB_LOCATION [REMOVE] Removed call re-negotiation code
 as it seemed to not work as intended

---
 pyVoIP/RTP.py        |   8 +-
 pyVoIP/SIP/client.py |  26 +++++--
 pyVoIP/VoIP/phone.py |  37 ++++------
 pyVoIP/sock/sock.py  | 170 +++++++++++++++++++++++--------------------
 4 files changed, 131 insertions(+), 110 deletions(-)

diff --git a/pyVoIP/RTP.py b/pyVoIP/RTP.py
index c146900..ddccf3b 100644
--- a/pyVoIP/RTP.py
+++ b/pyVoIP/RTP.py
@@ -349,8 +349,12 @@ def start(self) -> None:
 
     def stop(self) -> None:
         self.NSD = False
-        self.sin.close()
-        self.sout.close()
+        if hasattr(self, "sin"):
+            if self.sin:
+                self.sin.close()
+        if hasattr(self, "sout"):
+            if self.sout:
+                self.sout.close()
 
     def read(self, length: int = 160, blocking: bool = True) -> bytes:
         if not blocking:
diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py
index 1229538..7b73ccd 100644
--- a/pyVoIP/SIP/client.py
+++ b/pyVoIP/SIP/client.py
@@ -49,7 +49,9 @@ def __init__(
         hostname: Optional[str] = None,
         remote_hostname: Optional[str] = None,
         bind_port=5060,
-        call_callback: Optional[Callable[[SIPMessage], Optional[str]]] = None,
+        call_callback: Optional[
+            Callable[["VoIPConnection", SIPMessage], Optional[str]]
+        ] = None,
         transport_mode: TransportMode = TransportMode.UDP,
         cert_file: Optional[str] = None,
         key_file: Optional[str] = None,
@@ -120,6 +122,20 @@ def recv(self) -> None:
                     if pyVoIP.DEBUG:
                         raise
 
+    def handle_new_connection(self, conn: "VoIPConnection") -> None:
+        message = SIPMessage(conn.peak())
+        if message.type == SIPMessageType.REQUEST:
+            if message.method == "INVITE":
+                self._handle_invite(conn)
+
+    def _handle_invite(self, conn: "VoIPConnection") -> None:
+        message = SIPMessage(conn.peak())
+        if self.call_callback is None:
+            request = self.gen_busy(message)
+            conn.send(request)
+        else:
+            self.call_callback(conn, message)
+
     def parse_message(self, message: SIPMessage) -> None:
         if message.type != SIPMessageType.REQUEST:
             if message.status in (
@@ -143,12 +159,6 @@ def parse_message(self, message: SIPMessage) -> None:
                     "TODO: Add 500 Error on Receiving SIP Response",
                 )
             return
-        elif message.method == "INVITE":
-            if self.call_callback is None:
-                request = self.gen_busy(message)
-                self.sendto(request, message.headers["Via"]["address"])
-            else:
-                self.call_callback(message)
         elif message.method == "BYE":
             # TODO: If callCallback is None, the call doesn't exist, 481
             if self.call_callback:
@@ -192,7 +202,7 @@ def start(self) -> None:
             self.transport_mode,
             self.bind_ip,
             self.bind_port,
-            self.nat,
+            self,
             self.cert_file,
             self.key_file,
             self.key_password,
diff --git a/pyVoIP/VoIP/phone.py b/pyVoIP/VoIP/phone.py
index 9833bdc..fb441db 100644
--- a/pyVoIP/VoIP/phone.py
+++ b/pyVoIP/VoIP/phone.py
@@ -1,6 +1,7 @@
 from enum import Enum
 from pyVoIP import SIP, RTP
 from pyVoIP.credentials import CredentialsManager
+from pyVoIP.sock.sock import VoIPConnection
 from pyVoIP.sock.transport import TransportMode
 from pyVoIP.types import KEY_PASSWORD
 from pyVoIP.VoIP.call import CallState, VoIPCall
@@ -101,12 +102,14 @@ def __init__(
             transport_mode=self.transport_mode,
         )
 
-    def callback(self, request: SIP.SIPMessage) -> Optional[str]:
+    def callback(
+        self, conn: VoIPConnection, request: SIP.SIPMessage
+    ) -> Optional[str]:
         # debug("Callback: "+request.summary())
         if request.type == pyVoIP.SIP.SIPMessageType.REQUEST:
             # debug("This is a message")
             if request.method == "INVITE":
-                self._callback_MSG_Invite(request)
+                self._callback_MSG_Invite(conn, request)
             elif request.method == "BYE":
                 self._callback_MSG_Bye(request)
             elif request.method == "OPTIONS":
@@ -131,23 +134,13 @@ def callback(self, request: SIP.SIPMessage) -> Optional[str]:
     def get_status(self) -> PhoneStatus:
         return self._status
 
-    def _callback_MSG_Invite(self, request: SIP.SIPMessage) -> None:
+    def _callback_MSG_Invite(
+        self, conn: VoIPConnection, request: SIP.SIPMessage
+    ) -> None:
         call_id = request.headers["Call-ID"]
-        if call_id in self.calls:
-            debug("Re-negotiation detected!")
-            # TODO: this seems "dangerous" if for some reason sip server
-            # handles 2 and more bindings it will cause duplicate RTP-Clients
-            # to spawn.
-
-            # CallState.Ringing seems important here to prevent multiple
-            # answering and RTP-Client spawning. Find out when renegotiation
-            # is relevant.
-            if self.calls[call_id].state != CallState.RINGING:
-                self.calls[call_id].renegotiate(request)
-            return  # Raise Error
         if self.callClass is None:
             message = self.sip.gen_busy(request)
-            self.sip.sendto(message, request.headers["Via"][0]["address"])
+            conn.send(message)
         else:
             debug("New call!")
             sess_id = None
@@ -157,8 +150,8 @@ def _callback_MSG_Invite(self, request: SIP.SIPMessage) -> None:
                     self.session_ids.append(proposed)
                     sess_id = proposed
             message = self.sip.gen_ringing(request)
-            self.sip.sendto(message, request.headers["Via"][0]["address"])
-            call = self._create_Call(request, sess_id)
+            conn.send(message)
+            call = self._create_call(conn, request, sess_id)
             try:
                 t = Timer(1, call.ringing, [request])
                 t.name = f"Phone Call: {call_id}"
@@ -167,9 +160,8 @@ def _callback_MSG_Invite(self, request: SIP.SIPMessage) -> None:
                 self.threadLookup[t] = call_id
             except Exception:
                 message = self.sip.gen_busy(request)
-                self.sip.sendto(
+                conn.send(
                     message,
-                    request.headers["Via"][0]["address"],
                 )
                 raise
 
@@ -275,7 +267,9 @@ def _callback_RESP_Unavailable(self, request: SIP.SIPMessage) -> None:
         ack = self.sip.gen_ack(request)
         self.sip.sendto(ack)
 
-    def _create_Call(self, request: SIP.SIPMessage, sess_id: int) -> VoIPCall:
+    def _create_call(
+        self, conn: VoIPConnection, request: SIP.SIPMessage, sess_id: int
+    ) -> VoIPCall:
         """
         Create VoIP call object. Should be separated to enable better
         subclassing.
@@ -287,6 +281,7 @@ def _create_Call(self, request: SIP.SIPMessage, sess_id: int) -> VoIPCall:
             request,
             sess_id,
             self.bind_ip,
+            conn=conn,
             sendmode=self.recvmode,
         )
         return self.calls[call_id]
diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index d682c78..8a74833 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -1,5 +1,4 @@
-from typing import List, Optional, Tuple, Union
-from pyVoIP import SIP_STATE_DB_LOCATION
+from typing import TYPE_CHECKING, List, Optional, Tuple, Union
 from pyVoIP.types import KEY_PASSWORD, SOCKETS
 from pyVoIP.SIP import SIPMessage, SIPMessageType
 from pyVoIP.SIP.error import SIPParseError
@@ -16,6 +15,10 @@
 import time
 
 
+if TYPE_CHECKING:
+    from pyVoIP.SIP.client import SIPClient
+
+
 debug = pyVoIP.debug
 
 
@@ -38,6 +41,7 @@ def __init__(
         self.local_tag, self.remote_tag = self.sock.determine_tags(
             self.message
         )
+        self._peak_buffer: Optional[bytes] = None
         if conn and message.type == SIPMessageType.REQUEST:
             if self.sock.mode.tls_mode:
                 client_context = ssl.create_default_context()
@@ -66,85 +70,80 @@ def send(self, data: Union[bytes, str]) -> None:
             self.conn.send(data)
         debug(f"SENT:\n{msg.summary()}")
 
-    def __find_remote_tag(self) -> None:
-        if self.remote_tag is not None:
-            return
-        conn = self.sock.buffer.cursor()
-        result = conn.execute(
-            """SELECT "remote_tag" FROM "listening" WHERE
-                "call_id" = ?
-                AND "local_tag" = ?""",
-            (self.call_id, self.local_tag),
-        )
-        rows = result.fetchall()
-        if rows:
-            # print(f"Found remote: {rows[0][0]}")
-            self.remote_tag = rows[0][0]
+    def update_tags(self, local_tag: str, remote_tag: str) -> None:
+        self.local_tag = local_tag
+        self.remote_tag = remote_tag
 
-    def recv(self, nbytes: int, timeout=0) -> bytes:
-        timeout = time.monotonic() + timeout if timeout else math.inf
-        if self.conn:
-            # TODO: Timeout
-            msg = None
-            while not msg and not self.sock.SD:
-                data = self.conn.recv(nbytes)
-                try:
-                    msg = SIPMessage(data)
-                except SIPParseError as e:
-                    br = self.sock.gen_bad_request(
-                        connection=self, error=e, received=data
-                    )
-                    self.send(br)
-            if time.monotonic() >= timeout:
-                raise TimeoutError()
-            debug(f"RECEIVED:\n{msg.summary()}")
+    def peak(self) -> bytes:
+        return self.recv(8192, timeout=60, peak=True)
+
+    def recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
+        if self._peak_buffer:
+            data = self._peak_buffer
+            if not peak:
+                self._peak_buffer = None
             return data
+        if self.conn:
+            return self._tcp_tls_recv(nbytes, timeout, peak)
         else:
-            self.__find_remote_tag()
-            while time.monotonic() <= timeout and not self.sock.SD:
-                # print("Trying to receive")
-                # print(self.sock.get_database_dump())
-                conn = self.sock.buffer.cursor()
-                conn.row_factory = sqlite3.Row
-                sql = (
-                    'SELECT * FROM "msgs" WHERE "call_id"=? AND "local_tag"=?'
-                )
-                if self.remote_tag:
-                    sql += (
-                        ' UNION SELECT * FROM "msgs" WHERE "call_id"=? AND '
-                        + '"local_tag"=? AND "remote_tag"=?'
-                    )
-                bindings = (
-                    (
-                        self.call_id,
-                        self.local_tag,
-                        self.call_id,
-                        self.local_tag,
-                        self.remote_tag,
-                    )
-                    if self.remote_tag
-                    else (self.call_id, self.local_tag)
+            return self._udp_recv(nbytes, timeout, peak)
+
+    def _tcp_tls_recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
+        # TODO: Timeout
+        msg = None
+        while not msg and not self.sock.SD:
+            data = self.conn.recv(nbytes)
+            try:
+                msg = SIPMessage(data)
+            except SIPParseError as e:
+                br = self.sock.gen_bad_request(
+                    connection=self, error=e, received=data
                 )
-                result = conn.execute(sql, bindings)
-                row = result.fetchone()
-                if not row:
-                    conn.close()
-                    continue
-                try:
-                    self.sock.buffer.commit()
-                    conn.execute(
-                        'DELETE FROM "msgs" WHERE "id" = ?', (row["id"],)
-                    )
-                    self.sock.buffer.commit()
-                except sqlite3.OperationalError:
-                    pass
+                self.send(br)
+        if time.monotonic() >= timeout:
+            raise TimeoutError()
+        if peak:
+            self._peak_buffer = data
+        debug(f"RECEIVED:\n{msg.summary()}")
+        return data
+
+    def _udp_recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
+        timeout = time.monotonic() + timeout if timeout else math.inf
+        while time.monotonic() <= timeout and not self.sock.SD:
+            # print("Trying to receive")
+            # print(self.sock.get_database_dump())
+            conn = self.sock.buffer.cursor()
+            conn.row_factory = sqlite3.Row
+            sql = (
+                'SELECT * FROM "msgs" WHERE "call_id"=? AND '
+                + '"local_tag" IS ? AND "remote_tag" IS ?'
+            )
+            bindings = (
+                self.call_id,
+                self.local_tag,
+                self.remote_tag,
+            )
+            result = conn.execute(sql, bindings)
+            row = result.fetchone()
+            if not row:
+                conn.close()
+                continue
+            if peak:
+                # If peaking, return before deleting from the database
                 conn.close()
                 return row["msg"].encode("utf8")
-            if time.monotonic() >= timeout:
-                raise TimeoutError()
+            try:
+                self.sock.buffer.commit()
+                conn.execute('DELETE FROM "msgs" WHERE "id" = ?', (row["id"],))
+                self.sock.buffer.commit()
+            except sqlite3.OperationalError:
+                pass
+            conn.close()
+            return row["msg"].encode("utf8")
+        if time.monotonic() >= timeout:
+            raise TimeoutError()
 
     def close(self):
-        self.__find_remote_tag()
         self.sock.deregister_connection(self)
         if self.conn:
             self.conn.close()
@@ -156,7 +155,7 @@ def __init__(
         mode: TransportMode,
         bind_ip: str,
         bind_port: int,
-        nat: NAT,
+        sip: "SIPClient",
         cert_file: Optional[str] = None,
         key_file: Optional[str] = None,
         key_password: KEY_PASSWORD = None,
@@ -170,7 +169,8 @@ def __init__(
         self.s = socket.socket(socket.AF_INET, mode.socket_type)
         self.bind_ip: str = bind_ip
         self.bind_port: int = bind_port
-        self.nat = nat
+        self.sip = sip
+        self.nat: NAT = sip.nat
         self.server_context: Optional[ssl.SSLContext] = None
         if mode.tls_mode:
             self.server_context = ssl.SSLContext(
@@ -184,7 +184,7 @@ def __init__(
             self.s = self.server_context.wrap_socket(self.s, server_side=True)
 
         self.buffer = sqlite3.connect(
-            SIP_STATE_DB_LOCATION, check_same_thread=False
+            pyVoIP.SIP_STATE_DB_LOCATION, check_same_thread=False
         )
         """
         RFC 3261 Section 12, Paragraph 2 states:
@@ -209,7 +209,12 @@ def __init__(
             );"""
         )
         conn.execute(
-            """CREATE INDEX "msg_index" ON msgs ("call_id", "local_tag", "remote_tag");"""
+            """CREATE INDEX "msg_index" ON msgs """
+            + """("call_id", "local_tag", "remote_tag");"""
+        )
+        conn.execute(
+            """CREATE INDEX "msg_index_2" ON msgs """
+            + """("call_id", "remote_tag", "local_tag");"""
         )
         conn.execute(
             """CREATE TABLE "listening" (
@@ -271,12 +276,13 @@ def __get_connection(
                 sql = 'UPDATE "listening" SET "remote_tag" = ?, '
                 sql += '"local_tag" = ? WHERE "connection" = ?'
                 conn.execute(sql, (remote_tag, local_tag, rows[0][0]))
+                self.conns[rows[0][0]].update_tags(local_tag, remote_tag)
             conn.close()
             return self.conns[rows[0][0]]
         conn.close()
         return None
 
-    def __register_connection(self, connection: VoIPConnection) -> None:
+    def __register_connection(self, connection: VoIPConnection) -> int:
         self.conns_lock.acquire()
         self.conns.append(connection)
         conn_id = len(self.conns) - 1
@@ -311,6 +317,7 @@ def __register_connection(self, connection: VoIPConnection) -> None:
         finally:
             conn.close()
             self.conns_lock.release()
+            return conn_id
 
     def deregister_connection(self, connection: VoIPConnection) -> None:
         if self.mode is not TransportMode.UDP:
@@ -424,8 +431,11 @@ def _udp_run(self) -> None:
     def _handle_incoming_message(
         self, conn: Optional[SOCKETS], message: SIPMessage
     ):
+        conn_id = None
         if not self.__connection_exists(message):
-            self.__register_connection(VoIPConnection(self, conn, message))
+            conn_id = self.__register_connection(
+                VoIPConnection(self, conn, message)
+            )
 
         call_id = message.headers["Call-ID"]
         local_tag, remote_tag = self.determine_tags(message)
@@ -441,6 +451,8 @@ def _handle_incoming_message(
         except sqlite3.OperationalError:
             pass
         conn.close()
+        if conn_id:
+            self.sip.handle_new_connection(self.conns[conn_id])
 
     def run(self) -> None:
         self.bind((self.bind_ip, self.bind_port))

From a215093793e081f3c54eb909a8f2a1df171254de Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Fri, 29 Dec 2023 23:44:44 -0600
Subject: [PATCH 12/13] [FIX] Changed sqlite3 to use autocommit mode which
 fixed random errors

---
 pyVoIP/sock/sock.py | 16 +++-------------
 1 file changed, 3 insertions(+), 13 deletions(-)

diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index 8a74833..bb2cd4f 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -133,9 +133,7 @@ def _udp_recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
                 conn.close()
                 return row["msg"].encode("utf8")
             try:
-                self.sock.buffer.commit()
                 conn.execute('DELETE FROM "msgs" WHERE "id" = ?', (row["id"],))
-                self.sock.buffer.commit()
             except sqlite3.OperationalError:
                 pass
             conn.close()
@@ -184,7 +182,9 @@ def __init__(
             self.s = self.server_context.wrap_socket(self.s, server_side=True)
 
         self.buffer = sqlite3.connect(
-            pyVoIP.SIP_STATE_DB_LOCATION, check_same_thread=False
+            pyVoIP.SIP_STATE_DB_LOCATION,
+            isolation_level=None,
+            check_same_thread=False,
         )
         """
         RFC 3261 Section 12, Paragraph 2 states:
@@ -225,10 +225,6 @@ def __init__(
                 PRIMARY KEY("call_id", "local_tag", "remote_tag")
             );"""
         )
-        try:
-            self.buffer.commit()
-        except sqlite3.OperationalError:
-            pass
         conn.close()
         self.conns_lock = threading.Lock()
         self.conns: List[VoIPConnection] = []
@@ -300,7 +296,6 @@ def __register_connection(self, connection: VoIPConnection) -> int:
                     conn_id,
                 ),
             )
-            self.buffer.commit()
         except sqlite3.IntegrityError as e:
             e.add_note(
                 "Error is from registering connection for message: "
@@ -348,7 +343,6 @@ def deregister_connection(self, connection: VoIPConnection) -> None:
             conn.execute(
                 'DELETE FROM "listening" WHERE "connection" = ?', (conn_id,)
             )
-            self.buffer.commit()
         except sqlite3.OperationalError:
             pass
         finally:
@@ -446,10 +440,6 @@ def _handle_incoming_message(
             + "VALUES (?, ?, ?, ?)",
             (call_id, local_tag, remote_tag, raw_message),
         )
-        try:
-            self.buffer.commit()
-        except sqlite3.OperationalError:
-            pass
         conn.close()
         if conn_id:
             self.sip.handle_new_connection(self.conns[conn_id])

From 72cf17f67e267353f6e48ede198e5ec379d3d446 Mon Sep 17 00:00:00 2001
From: TJ Porter <taylerporter@gmail.com>
Date: Sat, 30 Dec 2023 00:15:01 -0600
Subject: [PATCH 13/13] [FIX] Fixed issue causing all TCP/TLS communications to
 isntantly timeout.

---
 pyVoIP/sock/sock.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pyVoIP/sock/sock.py b/pyVoIP/sock/sock.py
index bb2cd4f..8a9f4ff 100644
--- a/pyVoIP/sock/sock.py
+++ b/pyVoIP/sock/sock.py
@@ -89,6 +89,7 @@ def recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
             return self._udp_recv(nbytes, timeout, peak)
 
     def _tcp_tls_recv(self, nbytes: int, timeout=0, peak=False) -> bytes:
+        timeout = time.monotonic() + timeout if timeout else math.inf
         # TODO: Timeout
         msg = None
         while not msg and not self.sock.SD: