The book of Iris

  • I. Overview

    1. Philosophy
    2. Core concepts
    3. Run, Forrest, Run
  • II. Nuts and bolts

    1. Request/reply pattern
    2. Broadcast pattern
    3. Tunnel pattern
    4. Publish/subscribe
  • III. Skyscrapers & spaceships

    1. Iris inside docker
  • IV. Case studies

    1. Embree decentralized
    2. RegionRank internals
  • V. Epilogue

    1. The night is dark and full of terrors

Runtime environment

From its very inception, Iris was designed to simplify the development of cloud services. As such, the environmental assumptions that have been made are those present in consumer cloud platforms. Of course, Iris is applicable in private clouds and clusters too, but certain expectations need to be fulfilled to do so.

Network and firewall

A uniqueness of Iris is that it auto-discovers remote peers with which to form a network, without requiring manual configurations. This is done via address probing, searching the local network for possible candidates. This also means that Iris is restricted to IPv4 networks, since probing IPv6 addresses is not feasible; as well as being constrained to a single subnet, since it is too expensive to discover external ones.

At certain occasions though it is desirable to span subnets – or entire availability zones and data centers – providing geographical proximity and fail-over mechanisms for a cloud service. Such scenarios will be enabled by federated services, but further work is required for seamless integration into the Iris core.

Considering firewalls, all internal traffic is expected to be permitted between the hosts (protocol and port wise). This enables Iris to self-organize in a completely autonomous way, while delivering on the zero-configuration promise. This firewall and above subnet constraint may seem limiting at first, however, these are the exact environments provided by cloud operators as well as the usual setup of private compute clusters.

Mitigated threats

Even though – considering the outside world – all processes interacting with Iris are presumed to be run in a private and firewalled environment, Iris implements an elaborate security protocol, protecting all network traffic against various attack vectors.

Malicious entities are permitted in the private network, as long as they are not on the application hosts themselves. In other words, if the machines running Iris are penetrated, security is effectively considered compromised. Networks links are permitted to span through untrusted environments as long as the endpoints remain secured.

Given the above operational setup and assurances, the following threats are mitigated by Iris:

  • Wiretapping or sniffing attacks, whereby a transport-layer malicious entity captures the network traffic in transit and tries to extract information.
  • Impersonation or spoofing attacks whereby an attacker tries to trick the system into accepting unauthorized connections or deliver messages to wrong destinations.
  • Duplication or replay attacks, whereby a corrupted entity on a communication link resends a message, potentially executing some action multiple times.

System architecture

Architecture wise, messaging systems can usually be classified into one of two extremes. Some advocate a more centralized approach, where the middleware is isolated and protected from clients; whereas others prefer a more distributed approach, where responsibility is shared equally between participants. The compromise is client simplicity vs. scaling simplicity: a completely decentralized approach requires logic of a complexity beyond language agnosticism (i.e. hard to maintain in multiple languages).

Iris delivers a sweet spot between the two popular extremes. In order to achieve complete decentralization, every host participating in the distributed system is required to run an Iris node. However, opposed to classical peer-to-peer models, these nodes are not direct client applications, but gateways through which thin clients can communicate with each other. The power of this architecture is that it achieves scalability whilst maintaining client simplicity.

Iris nodes

At the core of the messaging model stand the Iris nodes, heavy weight processes that self-organize, form and maintain a peer-to-peer overlay network, responsible for all messaging logic ranging from service discovery, through load balancing till data security. These nodes implement the Iris protocol, a sophisticated peer-to-peer system.

This complex networking protocol however needs only a single implementation, as the Iris nodes act as gateways into the system, providing a network endpoint for all local clients to connect through. This endpoint speaks a so called relay protocol, a binary exchange format used to pass operation requests and responses between Iris nodes and client applications.

Client applications

Since client applications need only tell the Iris nodes what to do – but not how – they remain ignorant of the complex messaging logic involved, effectively becoming thin clients. The main benefit of this solution is language agnosticism, as client libraries have to implement only the simple relay protocol (specs to be released).

Furthermore, as long as the relay is fixed, the core Iris system can evolve freely without the worries of backward incompatible changes or breaking client code, granting Iris a potential and flexibility for growth and progress beyond that of existing middlewares.

Run, Forrest, Run

Being targeted for dynamically scaling cloud services, Iris strives towards zero configuration. The goal is that introducing additional nodes into the network should require no manual overhead whatsoever. To this end, Iris uses only two configuration options, both being the exact same for all instances: a name for the service and an RSA private key for security. The name ensures that multiple services can coexist within the same network, whilst the private key ensures all nodes are authenticated and all traffic encrypted.

> iris --help
Server node of the Iris decentralized cloud messaging framework.

Usage:
        iris [options]

The options are:
        -dev     [=false]    start in local developer mode (random cluster and key)
        -net                 name of the cluster to join or create
        -port    [=55555]    relay endpoint for locally connecting clients
        -rsa                 path to the RSA private key to use for data security

The command line variables might change slightly as Iris matures, but currently the name of the service to form is specified through the -net parameter, whilst the private key through -rsa. An optional argument -port can also be given to change the default endpoint where local clients connect.

Alternatively, Iris may be started in developer mode with the -dev option. This results in Iris generating a random service name and RSA private key. Local clients remain oblivious to dev mode, being able to use Iris to its fullest, only remote connections get disabled. The goal is to allow quick local testing without fancy configuration.

After starting up an Iris node, it will start searching the local network for possible remote instances (same service name), and organize into a peer-to-peer overlay network with all discovered and authenticated nodes. If a certain amount of time passes without discovering new nodes, Iris converges and opens its relay endpoint for local client to attach.

Finally, after the relay endpoint is opened, arbitrarily many clients written in arbitrary languages may attach to it and start sending messages through Iris. The official language bindings can be found on the projects downloads page, including source code, documentation and live playground. Each connecting entity may also decide whether it becomes a simple client only consuming the services provided by other participants, or a full fledged service, also making functionality available to others for consumption. Below you can find the code snippets for establishing both client (left), as well as service connections (right) to Iris through all the supported languages.

package goclient
import "gopkg.in/project-iris/iris-go.v1"

func main() {
  // Connect to the local relay on port 55555 as a client.  
  // A client cannot accept inbound requests, broadcasts
  // and tunnels, only initiate them.
  conn, err := iris.Connect(55555)
  if err != nil {
    panic(err)
  }
  // Ensure the connection is torn down
  defer conn.Close()
}
// Live: http://play.iris.karalabe.com/talks/binds/go.v1.slide#12
package goservice
import "gopkg.in/project-iris/iris-go.v1"

// Event handler implementing iris.ServiceHandler.
type callback struct{}
func (c *callback) Init(conn *iris.Connection) error { return nil }

// Event handlers not shown here: HandleRequest, HandleBroadcast,
// HandleTunnel and HandleDrop.

func main() {
  // Connect to the local relay on port 55555, join the "demo" service
  // cluster and assign a new event handler. The last argument defines
  // that the default service limits (thread/memory) are to be used.
  service, err := iris.Register(55555, "demo", new(callback), nil)
  if err != nil {
    panic(err)
  }
  // Ensure the service is unregistered
  defer service.Unregister()
}