Skip to content

Commit

Permalink
Add capability-based security to protect all TCP actions.
Browse files Browse the repository at this point in the history
Now we have the following hierarchy:

- `Env.Root`
  - `TCP.Connect.Auth`
    - `TCP.Connect.Ticket`
  - `TCP.Listen.Auth`
    - `TCP.Listen.Ticket`

Additionally we have `TCP.Accept.Ticket` which can only come from
an actual pending connection trying to connect to a TCP listener.

Attenuating through the hierarchy to create a connect ticket looks like this:
```savi
TCP.auth(env.root).connect.to(host, port)
```

Attentuating through the hierarchy to create a listen ticket looks like this:
```savi
TCP.auth(env.root).listen.on(host, port)
```
  • Loading branch information
jemc committed Mar 12, 2022
1 parent e684230 commit 807a670
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 32 deletions.
14 changes: 10 additions & 4 deletions spec/TCP.Spec.savi
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
:is IO.Actor(IO.Action)
:let env Env
:let io TCP.Listen.Engine
:new (@env): @io = TCP.Listen.Engine.new(@)
:new (@env)
// TODO: Why do tests hang when "localhost" is used instead of "127.0.0.1"?
@io = TCP.Listen.Engine.new(@
TCP.auth(@env.root).listen.on("127.0.0.1", "") // loopback, on any port
)

:be dispose: @io.close

:fun ref _io_react(action IO.Action)
case action == (
| IO.Action.Opened |
TCP.Spec.EchoClient.new(@env, Inspect[@io.local_port])
TCP.Spec.EchoClient.new(@env, Inspect[@io.listen_port_number])
@env.err.print("[Listener] Listening")
| IO.Action.OpenFailed |
@env.err.print("[Listener] Not listening:")
Expand Down Expand Up @@ -54,8 +58,10 @@
:is IO.Actor(IO.Action)
:let env Env
:let io TCP.Engine
:new (@env, service)
@io = TCP.Engine.connect(@, "localhost", service)
:new (@env, port)
@io = TCP.Engine.new(@
TCP.auth(@env.root).connect.to("localhost", port)
)

// TODO: Can we make this trigger _io_react with IO.Action.OpenFailed
// automatically via the same mechanism we will use for queuing later
Expand Down
16 changes: 16 additions & 0 deletions src/TCP.Accept.Ticket.savi
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
:: A `TCP.Accept.Ticket` grants the capability to accept an incoming connection.
:: It is granted by `TCP.Listen.Engine.pending_connections` when there is
:: a remote TCP sockets attempting to connect to the listener.
::
:: If the ticket-holder wishes to accept the connection, it should be passed
:: to an actor that will use `TCP.Engine.accept` to create a new engine
:: that can be used to exchange data with the remote TCP socket.
::
:: If the ticket-holder does not wish to accept the connection, it must be
:: explicitly rejected using the `reject` method, which consumes the ticket
:: and frees up resources associated with the attempted connection.
:struct iso TCP.Accept.Ticket
:let _listener IO.Actor(IO.Action)
:let _fd U32
:new iso _new(@_listener, @_fd)

// TODO: This struct should allow inspecting information about the
// attempted connection, such as the remote IP address, for example.
// This would allow the ticket-holder to make an informed decision
// to either accept or reject the connection based on that information.

:: Reject this attempted connection instead of accepting it into an engine.
::
:: This destroys the ticket, closes the underlying socket, and notifies
Expand Down
22 changes: 22 additions & 0 deletions src/TCP.Auth.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:: `TCP.Auth` grants the capability to do unlimited actions related to TCP.
::
:: To grant only the capability to open connections to remote hosts,
:: attenuate this capability to `TCP.Connect.Auth` using the `connect` method.
::
:: To grant only the capability to bind a listener to accept connections,
:: attenuate this capability to `TCP.Listen.Auth` using the `listen` method.
::
:: Both of those lesser capabilities also have ways to attenuate further to
:: allow only a specific host and port to bind/connect to.
:struct val TCP.Auth // TODO: use :authority instead of an empty :struct
:: Use the given `Env.Root` (which is the root of all authority) to
:: attenuate to this lesser capability (which grants only TCP actions).
:new val (root Env.Root)

:: Use this capability (which grants unlimited TCP actions) to attenuate
:: to a `TCP.Connect.Auth` (which grants only for opening connections).
:fun val connect: TCP.Connect.Auth.new(@)

:: Use this capability (which grants unlimited TCP actions) to attenuate
:: to a `TCP.Listen.Auth` (which grants only for binding listeners).
:fun val listen: TCP.Listen.Auth.new(@)
27 changes: 27 additions & 0 deletions src/TCP.Connect.Auth.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
:: `TCP.Connect.Auth` grants the capability to open any number of TCP connection
:: sockets to any remote hosts/ports.
::
:: To grant the capability to open just one TCP connection, attenuate this
:: capability to a `TCP.Connect.Ticket`, using the `to` method.
:struct val TCP.Connect.Auth // TODO: use :authority instead of an empty :struct

:: Use the given `TCP.Auth` (which grants the capability for all TCP actions)
:: to attenuate to this lesser capability (which grants only TCP connections).
:new val (auth TCP.Auth)

:: Use this capability (which grants unlimited TCP connections) to attenuate
:: to a `TCP.Connect.Ticket` (which grants for only a single host and port).
::
:: The `host` string indicates the remote host to connect to, either as an
:: IP address, or as a domain name to be resolved via DNS.
:: If `host` is empty, localhost (the loopback interface) will be targeted.
::
:: The `port` string may be a number string (such as `"80"`) or a named port
:: (such as `"http"`). See the IANA port number registry for more examples.
::
:: The `from_port`, if given, indicates the local port to bind to.
:: This is not usually necessary, but may be used if the remote side is
:: expected to validate the port that the connection comes from.
:: If `from_port` is left empty, an open port will be selected arbitrarily.
:fun val to(host String, port String, from_port String = "")
TCP.Connect.Ticket.new(@, host, port, from_port)
25 changes: 25 additions & 0 deletions src/TCP.Connect.Ticket.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:: A `TCP.Connect.Ticket` grants the capability to connect to a specific
:: host and port using the TCP protocol.
::
:: To make use of the ticket, it should be passed to an actor that will use
:: `TCP.Engine.new` to create a new engine that can be used to connect and
:: exchange data with the remote TCP socket (if the connection succeeds).
:struct iso TCP.Connect.Ticket
:: The `host` string indicates the remote host to connect to, either as an
:: IP address, or as a domain name to be resolved via DNS.
:: If `host` is empty, localhost (the loopback interface) will be targeted.
:let host String

:: The `port` string may be a number string (such as `"80"`) or a named port
:: (such as `"http"`). See the IANA port number registry for more examples.
:let port String

:: The `from_port`, if given, indicates the local port to bind to.
:: This is not usually necessary, but may be used if the remote side is
:: expected to validate the port that the connection comes from.
:: If `from_port` is left empty, an open port will be selected arbitrarily.
:let from_port String

:: Use the given `TCP.Connect.Auth` (which grants unlimited TCP connections)
:: to issue a new ticket (which grants for only a single host and port).
:new iso new(auth TCP.Connect.Auth, @host, @port, @from_port = "")
36 changes: 17 additions & 19 deletions src/TCP.Engine.savi
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,30 @@
:let read_stream: ByteStream.Reader.new
:let write_stream ByteStream.Writer

:fun non connect(
// TODO: TCPConnectionAuth, rather than ambient authority.
actor IO.Actor(IO.Action)
host String
service String
from String = ""
)
try (
@_new_with_io(IO.CoreEngine.new_tcp_connect!(actor, host, service, from))
:new (actor IO.Actor(IO.Action), ticket TCP.Connect.Ticket)
@io = try (
// TODO: The IO package shouldn't expose this unsafe interface that
// could be used to circumvent the capability security of the TCP package.
// Instead, the relevant code should be carefully moved to this package.
IO.CoreEngine.new_tcp_connect!(
actor
ticket.host
ticket.port
ticket.from_port
)
|
invalid = @_new_with_io(IO.CoreEngine.new)
invalid.connect_error = OSError.EINVAL
invalid
@connect_error = OSError.EINVAL
IO.CoreEngine.new // an invalid one
)
@write_stream = ByteStream.Writer.new(@io)

:fun non accept(
:new accept(
actor IO.Actor(IO.Action)
ticket TCP.Accept.Ticket
)
io = IO.CoreEngine.new_from_fd_rw(actor, ticket._fd)
new = @_new_with_io(io)
new._listener = ticket._listener
new

:new _new_with_io(@io)
@io = IO.CoreEngine.new_from_fd_rw(actor, ticket._fd)
@write_stream = ByteStream.Writer.new(@io)
@_listener = ticket._listener

:fun ref react(event CPointer(AsioEvent), flags U32, arg U32) @
:yields IO.Action
Expand Down
25 changes: 25 additions & 0 deletions src/TCP.Listen.Auth.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:: `TCP.Listen.Auth` grants the capability to open any number of TCP listener
:: sockets on any interfaces/ports, accepting any number of remote connections.
::
:: To grant the capability to open just one TCP listener socket, attenuate this
:: capability to a `TCP.Listen.Ticket`, using the `on` method.
:struct val TCP.Listen.Auth // TODO: use :authority instead of an empty :struct

:: Use the given `TCP.Auth` (which grants the capability for all TCP actions)
:: to attenuate to this lesser capability (which grants only TCP listeners).
:new val (auth TCP.Auth)

:: Use this capability (which grants unlimited TCP listeners) to attenuate
:: to a `TCP.Listen.Ticket` (which grants for only a single host and port).
::
:: The `host` string is an indirect indicator of which interface to bind to.
:: For example, `"localhost"` indicates the loopback interface (allowing no
:: connections from remote origins), whereas `"0.0.0.0"` or `"::"` indicate
:: to the listener to bind on all interfaces (allowing remote connections).
:: If `host` is empty, the listener will bind on all interfaces.
::
:: The `port` string may be a number string (such as `"80"`) or a named port
:: (such as `"http"`). See the IANA port number registry for more examples.
:: If `port` is an empty string, an open port will be selected arbitrarily.
:fun val on(host String, port String)
TCP.Listen.Ticket.new(@, host, port)
16 changes: 7 additions & 9 deletions src/TCP.Listen.Engine.savi
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@
:var _paused Bool: False

:var listen_error OSError: OSError.None
:fun local_port: _NetAddress._for_fd(@_fd).port
:fun listen_port_number: _NetAddress._for_fd(@_fd).port // TODO: what happens if @_fd is invalid (-1)?

:new (
// TODO: TCP.Listener.Auth, rather than ambient authority.
@_actor
host String = ""
service String = "0"
@_limit = 0
)
event = _LibPonyOS.pony_os_listen_tcp(@_actor, host.cstring, service.cstring)
:new (@_actor, ticket TCP.Listen.Ticket, @_limit = 0)
event = _LibPonyOS.pony_os_listen_tcp(
@_actor
ticket.host.cstring
ticket.port.cstring
)
if event.is_not_null (
@_event = event
@_fd = AsioEvent.fd(@_event)
Expand Down
22 changes: 22 additions & 0 deletions src/TCP.Listen.Ticket.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:: A `TCP.Listen.Ticket` grants the capability to bind a TCP listener on
:: a specific port and host (which implies the interface to bind to).
::
:: To make use of the ticket, it should be passed to an actor that will use
:: `TCP.Listen.Engine.new` to create a new engine that binds the listener and
:: can issue `TCP.Accept.Ticket`s when new pending connections are initiated.
:struct iso TCP.Listen.Ticket
:: The `host` string is an indirect indicator of which interface to bind to.
:: For example, `"localhost"` indicates the loopback interface (allowing no
:: connections from remote origins), whereas `"0.0.0.0"` or `"::"` indicate
:: to the listener to bind on all interfaces (allowing remote connections).
:: If `host` is empty, the listener will bind on all interfaces.
:let host String

:: The `port` string may be a number string (such as `"80"`) or a named port
:: (such as `"http"`). See the IANA port number registry for more examples.
:: If `port` is an empty string, an open port will be selected arbitrarily.
:let port String

:: Use the given `TCP.Listen.Auth` (whichwhich grants unlimited TCP listeners)
:: to issue a new ticket (which grants for only a single host and port).
:new iso new(auth TCP.Listen.Auth, @host, @port)
6 changes: 6 additions & 0 deletions src/TCP.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:: The `TCP` module is the namespace for this library, and is also a place
:: for simple convenience functions that wrap other functions in the library.
:module TCP
:: Use the given `Env.Root` (which is the root of all authority) to
:: attenuate to a `TCP.Auth` (which grants only TCP actions).
:fun auth(root): TCP.Auth.new(root)

0 comments on commit 807a670

Please sign in to comment.