Skip to content

Commit

Permalink
Add net.TCPConnection & remove TCPIPConnection
Browse files Browse the repository at this point in the history
This adds a new TCP client connection actor which replaces the older
TCPIPConnection actor. It improves on the old implementation through the
addition of DNS resolution and Happy Eyeballs (RFC6555).

While not super complex it is non-trivial compared to the old connection
actor and so there are probably bugs lurking. More test cases are needed.

Right now we do a rather aggressive Happy Eyeballs where we instantly
connect both to IPv4 and IPv6. This effectively doubles the resources
consumed, fds on our end and more on the server side. While the RFC
allows for this, the recommendation is to have a slight delay for the
IPv4 side, like I think Chrome does 300ms or so. We can look into that.

Added net.is_ipv4() and net.is_ipv6() as well.

Originally had ideas about just adding this and letting the older
TCPIPConnection remain but as we've made backwards incompatible changes
anyway, it's just easier to replace it right away.
  • Loading branch information
plajjan committed Aug 5, 2023
1 parent 991188f commit 0a59515
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 75 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,25 @@
## Unreleased

### Added
- `net.TCPConnection`: A new TCP client connection actor [#1398]
- Support DNS lookups so the input address can be a name rather than an IP
address
- It is still possible to connect using an IP address
- Supports Happy Eyeballs (RFC6555) which uses both IPv4 and IPv6
simultaneously to connect using the fastest transport
- Happy Eyeballs is only used for hostnames. If an IP address is provided,
we connect directly to it.
- `net.is_ipv4(address: str) -> bool` tells you if something is an IPv4 address
- `net.is_ipv6(address: str) -> bool` tells you if something is an IPv6 address
- AbE: Documented capability based security [#1267]

### Changed
- `net.TCPIPConnection` is removed and replaced by `net.TCPConnection`
- Originally opted to add `net.TCPConnection` now and phase out
`net.TCPIPConnection` later but as there is already a breaking change with
the change of Auth -> Cap so all user code related to networking (and other
things) need to be changed, we might as well change from
`net.TCPIPConnection` to `net.TCPConnection`
- `DNS` actor is replaced with lookup functions [#1406]
- The `DNS` actor is removed, the `lookup_a` & `lookup_aaaa` methods it has
are now free functions in the `net` module, i.e. simply call `net.lookup_a`
Expand Down
126 changes: 117 additions & 9 deletions base/src/net.act
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class _TCPListenConnectCap():
"""Internal use only"""
pass

def is_ipv4(address: str) -> bool:
NotImplemented

def is_ipv6(address: str) -> bool:
NotImplemented

def _lookup_a(name: str, on_resolve: action(list[str]) -> None, on_error: action(str, str) -> None) -> None:
"""Perform DNS lookup for name of record type A
Expand All @@ -53,32 +58,135 @@ def lookup_aaaa(cap: DNSCap, name: str, on_resolve: action(list[str]) -> None, o
_lookup_aaaa(name, on_resolve, on_error)


actor TCPIPConnection(cap: TCPConnectCap, address: str, port: int, on_connect: action(TCPIPConnection) -> None, on_receive: action(TCPIPConnection, bytes) -> None, on_error: action(TCPIPConnection, str) -> None):
actor TCPConnection(cap: TCPConnectCap, address: str, port: int, on_connect: action(TCPConnection) -> None, on_receive: action(TCPConnection, bytes) -> None, on_error: action(TCPConnection, str) -> None):
"""TCP IP Connection"""
_socket = -1
var _a_res: list[str] = []
var _aaaa_res: list[str] = []
var _sock = -1
var _sock4 = -1
var _sock4_state = 0
var _sock4_error = ""
var _sock6 = -1
var _sock6_state = 0
var _sock6_error = ""
var _state = 0

var _connections = 0
var _bytes_in: u64 = 0
var _bytes_out: u64 = 0

proc def _pin_affinity() -> None:
NotImplemented
_pin_affinity()

action def close(on_close: action(TCPIPConnection) -> None) -> None:
"""Close the connection"""
# DNS A
def _on_dns_a_resolve(result):
_a_res = result
_connect4(result[0])

def _on_dns_a_error(name, msg):
_lookup_a(address, _on_dns_a_resolve, _on_dns_a_error)

# DNS AAAA
def _on_dns_aaaa_resolve(result):
_aaaa_res = result
_connect6(result[0])

def _on_dns_aaaa_error(name, msg):
_lookup_aaaa(address, _on_dns_aaaa_resolve, _on_dns_aaaa_error)

action def _on_tcp_error(sockfamily: int, err: int, errmsg: str):
if sockfamily == 4:
if _sock6_error != "":
on_error(self, errmsg + " IPv6 error: " + _sock6_error)
_sock4_error = errmsg
elif sockfamily == 6:
if _sock4_error != "":
on_error(self, errmsg + " IPv4 error: " + _sock4_error)
_sock6_error = errmsg


# TCP connect over IPv4
proc def _connect4(ip_address: str):
NotImplemented

def reconnect():
close(_connect)
action def _on_connect4():
_sock4_error = ""
_on_connect(_sock4)

# TCP connect over IPv6
proc def _connect6(ip_address: str):
NotImplemented

action def _on_connect6():
_sock6_error = ""
_on_connect(_sock6)

# Handle on_connect event
proc def _on_connect(sock: int):
if _state == 2:
pass
elif _state == 1:
_state = 2
_sock = sock
_read_start()
_connections += 1
on_connect(self)
else:
print("Unexpected state", _state)

proc def _read_start():
NotImplemented

action def write(data: bytes) -> None:
"""Write data to remote"""
NotImplemented

action def close(on_close: action(TCPConnection) -> None) -> None:
"""Close the connection"""
NotImplemented

def reconnect():
close(_connect)

def _connect(c):
_state = 1
if is_ipv4(address):
_connect4(address)
elif is_ipv6(address):
_connect6(address)
else:
_lookup_aaaa(address, _on_dns_aaaa_resolve, _on_dns_aaaa_error)
_lookup_a(address, _on_dns_a_resolve, _on_dns_a_error)

mut def __resume__() -> None:
NotImplemented

proc def _init():
action def ip_version() -> ?int:
NotImplemented

proc def _connect(c):
action def local_address() -> str:
NotImplemented
_init()

action def remote_address() -> str:
NotImplemented

action def metrics() -> list[u64]:
return [
_connections,
_bytes_in,
_bytes_out
]

# TODO: get rid of this, but how to call _on_connect4 directly?
var _fun_oncon4: action() -> None = _on_connect4
var _fun_oncon6: action() -> None = _on_connect6
var _fun_on_tcp_error: action(int, int, str) -> None = _on_tcp_error

_connect(self)



actor TCPListenConnection(cap: _TCPListenConnectCap, server_client: int):
"""TCP Listener Connection"""
var client: int = -1
Expand Down
Loading

0 comments on commit 0a59515

Please sign in to comment.