Skip to content

Commit 807a670

Browse files
committed
Add capability-based security to protect all TCP actions.
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) ```
1 parent e684230 commit 807a670

10 files changed

+177
-32
lines changed

spec/TCP.Spec.savi

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
:is IO.Actor(IO.Action)
33
:let env Env
44
:let io TCP.Listen.Engine
5-
:new (@env): @io = TCP.Listen.Engine.new(@)
5+
:new (@env)
6+
// TODO: Why do tests hang when "localhost" is used instead of "127.0.0.1"?
7+
@io = TCP.Listen.Engine.new(@
8+
TCP.auth(@env.root).listen.on("127.0.0.1", "") // loopback, on any port
9+
)
610

711
:be dispose: @io.close
812

913
:fun ref _io_react(action IO.Action)
1014
case action == (
1115
| IO.Action.Opened |
12-
TCP.Spec.EchoClient.new(@env, Inspect[@io.local_port])
16+
TCP.Spec.EchoClient.new(@env, Inspect[@io.listen_port_number])
1317
@env.err.print("[Listener] Listening")
1418
| IO.Action.OpenFailed |
1519
@env.err.print("[Listener] Not listening:")
@@ -54,8 +58,10 @@
5458
:is IO.Actor(IO.Action)
5559
:let env Env
5660
:let io TCP.Engine
57-
:new (@env, service)
58-
@io = TCP.Engine.connect(@, "localhost", service)
61+
:new (@env, port)
62+
@io = TCP.Engine.new(@
63+
TCP.auth(@env.root).connect.to("localhost", port)
64+
)
5965

6066
// TODO: Can we make this trigger _io_react with IO.Action.OpenFailed
6167
// automatically via the same mechanism we will use for queuing later

src/TCP.Accept.Ticket.savi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1+
:: A `TCP.Accept.Ticket` grants the capability to accept an incoming connection.
2+
:: It is granted by `TCP.Listen.Engine.pending_connections` when there is
3+
:: a remote TCP sockets attempting to connect to the listener.
4+
::
5+
:: If the ticket-holder wishes to accept the connection, it should be passed
6+
:: to an actor that will use `TCP.Engine.accept` to create a new engine
7+
:: that can be used to exchange data with the remote TCP socket.
8+
::
9+
:: If the ticket-holder does not wish to accept the connection, it must be
10+
:: explicitly rejected using the `reject` method, which consumes the ticket
11+
:: and frees up resources associated with the attempted connection.
112
:struct iso TCP.Accept.Ticket
213
:let _listener IO.Actor(IO.Action)
314
:let _fd U32
415
:new iso _new(@_listener, @_fd)
516

17+
// TODO: This struct should allow inspecting information about the
18+
// attempted connection, such as the remote IP address, for example.
19+
// This would allow the ticket-holder to make an informed decision
20+
// to either accept or reject the connection based on that information.
21+
622
:: Reject this attempted connection instead of accepting it into an engine.
723
::
824
:: This destroys the ticket, closes the underlying socket, and notifies

src/TCP.Auth.savi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
:: `TCP.Auth` grants the capability to do unlimited actions related to TCP.
2+
::
3+
:: To grant only the capability to open connections to remote hosts,
4+
:: attenuate this capability to `TCP.Connect.Auth` using the `connect` method.
5+
::
6+
:: To grant only the capability to bind a listener to accept connections,
7+
:: attenuate this capability to `TCP.Listen.Auth` using the `listen` method.
8+
::
9+
:: Both of those lesser capabilities also have ways to attenuate further to
10+
:: allow only a specific host and port to bind/connect to.
11+
:struct val TCP.Auth // TODO: use :authority instead of an empty :struct
12+
:: Use the given `Env.Root` (which is the root of all authority) to
13+
:: attenuate to this lesser capability (which grants only TCP actions).
14+
:new val (root Env.Root)
15+
16+
:: Use this capability (which grants unlimited TCP actions) to attenuate
17+
:: to a `TCP.Connect.Auth` (which grants only for opening connections).
18+
:fun val connect: TCP.Connect.Auth.new(@)
19+
20+
:: Use this capability (which grants unlimited TCP actions) to attenuate
21+
:: to a `TCP.Listen.Auth` (which grants only for binding listeners).
22+
:fun val listen: TCP.Listen.Auth.new(@)

src/TCP.Connect.Auth.savi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
:: `TCP.Connect.Auth` grants the capability to open any number of TCP connection
2+
:: sockets to any remote hosts/ports.
3+
::
4+
:: To grant the capability to open just one TCP connection, attenuate this
5+
:: capability to a `TCP.Connect.Ticket`, using the `to` method.
6+
:struct val TCP.Connect.Auth // TODO: use :authority instead of an empty :struct
7+
8+
:: Use the given `TCP.Auth` (which grants the capability for all TCP actions)
9+
:: to attenuate to this lesser capability (which grants only TCP connections).
10+
:new val (auth TCP.Auth)
11+
12+
:: Use this capability (which grants unlimited TCP connections) to attenuate
13+
:: to a `TCP.Connect.Ticket` (which grants for only a single host and port).
14+
::
15+
:: The `host` string indicates the remote host to connect to, either as an
16+
:: IP address, or as a domain name to be resolved via DNS.
17+
:: If `host` is empty, localhost (the loopback interface) will be targeted.
18+
::
19+
:: The `port` string may be a number string (such as `"80"`) or a named port
20+
:: (such as `"http"`). See the IANA port number registry for more examples.
21+
::
22+
:: The `from_port`, if given, indicates the local port to bind to.
23+
:: This is not usually necessary, but may be used if the remote side is
24+
:: expected to validate the port that the connection comes from.
25+
:: If `from_port` is left empty, an open port will be selected arbitrarily.
26+
:fun val to(host String, port String, from_port String = "")
27+
TCP.Connect.Ticket.new(@, host, port, from_port)

src/TCP.Connect.Ticket.savi

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
:: A `TCP.Connect.Ticket` grants the capability to connect to a specific
2+
:: host and port using the TCP protocol.
3+
::
4+
:: To make use of the ticket, it should be passed to an actor that will use
5+
:: `TCP.Engine.new` to create a new engine that can be used to connect and
6+
:: exchange data with the remote TCP socket (if the connection succeeds).
7+
:struct iso TCP.Connect.Ticket
8+
:: The `host` string indicates the remote host to connect to, either as an
9+
:: IP address, or as a domain name to be resolved via DNS.
10+
:: If `host` is empty, localhost (the loopback interface) will be targeted.
11+
:let host String
12+
13+
:: The `port` string may be a number string (such as `"80"`) or a named port
14+
:: (such as `"http"`). See the IANA port number registry for more examples.
15+
:let port String
16+
17+
:: The `from_port`, if given, indicates the local port to bind to.
18+
:: This is not usually necessary, but may be used if the remote side is
19+
:: expected to validate the port that the connection comes from.
20+
:: If `from_port` is left empty, an open port will be selected arbitrarily.
21+
:let from_port String
22+
23+
:: Use the given `TCP.Connect.Auth` (which grants unlimited TCP connections)
24+
:: to issue a new ticket (which grants for only a single host and port).
25+
:new iso new(auth TCP.Connect.Auth, @host, @port, @from_port = "")

src/TCP.Engine.savi

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,30 @@
66
:let read_stream: ByteStream.Reader.new
77
:let write_stream ByteStream.Writer
88

9-
:fun non connect(
10-
// TODO: TCPConnectionAuth, rather than ambient authority.
11-
actor IO.Actor(IO.Action)
12-
host String
13-
service String
14-
from String = ""
15-
)
16-
try (
17-
@_new_with_io(IO.CoreEngine.new_tcp_connect!(actor, host, service, from))
9+
:new (actor IO.Actor(IO.Action), ticket TCP.Connect.Ticket)
10+
@io = try (
11+
// TODO: The IO package shouldn't expose this unsafe interface that
12+
// could be used to circumvent the capability security of the TCP package.
13+
// Instead, the relevant code should be carefully moved to this package.
14+
IO.CoreEngine.new_tcp_connect!(
15+
actor
16+
ticket.host
17+
ticket.port
18+
ticket.from_port
19+
)
1820
|
19-
invalid = @_new_with_io(IO.CoreEngine.new)
20-
invalid.connect_error = OSError.EINVAL
21-
invalid
21+
@connect_error = OSError.EINVAL
22+
IO.CoreEngine.new // an invalid one
2223
)
24+
@write_stream = ByteStream.Writer.new(@io)
2325

24-
:fun non accept(
26+
:new accept(
2527
actor IO.Actor(IO.Action)
2628
ticket TCP.Accept.Ticket
2729
)
28-
io = IO.CoreEngine.new_from_fd_rw(actor, ticket._fd)
29-
new = @_new_with_io(io)
30-
new._listener = ticket._listener
31-
new
32-
33-
:new _new_with_io(@io)
30+
@io = IO.CoreEngine.new_from_fd_rw(actor, ticket._fd)
3431
@write_stream = ByteStream.Writer.new(@io)
32+
@_listener = ticket._listener
3533

3634
:fun ref react(event CPointer(AsioEvent), flags U32, arg U32) @
3735
:yields IO.Action

src/TCP.Listen.Auth.savi

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
:: `TCP.Listen.Auth` grants the capability to open any number of TCP listener
2+
:: sockets on any interfaces/ports, accepting any number of remote connections.
3+
::
4+
:: To grant the capability to open just one TCP listener socket, attenuate this
5+
:: capability to a `TCP.Listen.Ticket`, using the `on` method.
6+
:struct val TCP.Listen.Auth // TODO: use :authority instead of an empty :struct
7+
8+
:: Use the given `TCP.Auth` (which grants the capability for all TCP actions)
9+
:: to attenuate to this lesser capability (which grants only TCP listeners).
10+
:new val (auth TCP.Auth)
11+
12+
:: Use this capability (which grants unlimited TCP listeners) to attenuate
13+
:: to a `TCP.Listen.Ticket` (which grants for only a single host and port).
14+
::
15+
:: The `host` string is an indirect indicator of which interface to bind to.
16+
:: For example, `"localhost"` indicates the loopback interface (allowing no
17+
:: connections from remote origins), whereas `"0.0.0.0"` or `"::"` indicate
18+
:: to the listener to bind on all interfaces (allowing remote connections).
19+
:: If `host` is empty, the listener will bind on all interfaces.
20+
::
21+
:: The `port` string may be a number string (such as `"80"`) or a named port
22+
:: (such as `"http"`). See the IANA port number registry for more examples.
23+
:: If `port` is an empty string, an open port will be selected arbitrarily.
24+
:fun val on(host String, port String)
25+
TCP.Listen.Ticket.new(@, host, port)

src/TCP.Listen.Engine.savi

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@
1212
:var _paused Bool: False
1313

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

17-
:new (
18-
// TODO: TCP.Listener.Auth, rather than ambient authority.
19-
@_actor
20-
host String = ""
21-
service String = "0"
22-
@_limit = 0
23-
)
24-
event = _LibPonyOS.pony_os_listen_tcp(@_actor, host.cstring, service.cstring)
17+
:new (@_actor, ticket TCP.Listen.Ticket, @_limit = 0)
18+
event = _LibPonyOS.pony_os_listen_tcp(
19+
@_actor
20+
ticket.host.cstring
21+
ticket.port.cstring
22+
)
2523
if event.is_not_null (
2624
@_event = event
2725
@_fd = AsioEvent.fd(@_event)

src/TCP.Listen.Ticket.savi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
:: A `TCP.Listen.Ticket` grants the capability to bind a TCP listener on
2+
:: a specific port and host (which implies the interface to bind to).
3+
::
4+
:: To make use of the ticket, it should be passed to an actor that will use
5+
:: `TCP.Listen.Engine.new` to create a new engine that binds the listener and
6+
:: can issue `TCP.Accept.Ticket`s when new pending connections are initiated.
7+
:struct iso TCP.Listen.Ticket
8+
:: The `host` string is an indirect indicator of which interface to bind to.
9+
:: For example, `"localhost"` indicates the loopback interface (allowing no
10+
:: connections from remote origins), whereas `"0.0.0.0"` or `"::"` indicate
11+
:: to the listener to bind on all interfaces (allowing remote connections).
12+
:: If `host` is empty, the listener will bind on all interfaces.
13+
:let host String
14+
15+
:: The `port` string may be a number string (such as `"80"`) or a named port
16+
:: (such as `"http"`). See the IANA port number registry for more examples.
17+
:: If `port` is an empty string, an open port will be selected arbitrarily.
18+
:let port String
19+
20+
:: Use the given `TCP.Listen.Auth` (whichwhich grants unlimited TCP listeners)
21+
:: to issue a new ticket (which grants for only a single host and port).
22+
:new iso new(auth TCP.Listen.Auth, @host, @port)

src/TCP.savi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:: The `TCP` module is the namespace for this library, and is also a place
2+
:: for simple convenience functions that wrap other functions in the library.
3+
:module TCP
4+
:: Use the given `Env.Root` (which is the root of all authority) to
5+
:: attenuate to a `TCP.Auth` (which grants only TCP actions).
6+
:fun auth(root): TCP.Auth.new(root)

0 commit comments

Comments
 (0)