Difference between revisions of "Unix sockets"

From iPhone Development Wiki
Jump to: navigation, search
(Choosing a port)
(link to the better CPDistributedMessagingCenter reference)
Line 19: Line 19:
 
* Takes more work to properly set up
 
* Takes more work to properly set up
  
If this is your first time doing IPC, it's probably better to use another method such as [[CPDistributedMessagingCenter]] or [[LightMessaging]], which are more straightforward.
+
If this is your first time doing IPC, it's probably better to use another method such as [[RocketBootstrap#CPDistributedMessagingCenter_Example|CPDistributedMessagingCenter]] or [[LightMessaging]], which are more straightforward.
 
Unix sockets should only really be used if you've used another IPC method and have found it lacking or restrictive.
 
Unix sockets should only really be used if you've used another IPC method and have found it lacking or restrictive.
  

Revision as of 10:37, 29 January 2018

Unix sockets are a low-level way of achieving inter-process communication on Unix systems, including iOS. All of the other types of IPC in iOS, e.g. mach ports and the like, are built on top of Unix sockets.

For more/better reading material, check out beej's Unix socket guide or his excellent network programming guide.

Comparison to other IPC methods

Pros

  • No dependencies
  • Extremely portable
  • Less overhead
  • "Easier" to bypass sandbox restrictions (more on this later)

Cons

  • API is in C instead of Objective-C
  • Not "the Apple way"
  • Takes more work to properly set up

If this is your first time doing IPC, it's probably better to use another method such as CPDistributedMessagingCenter or LightMessaging, which are more straightforward. Unix sockets should only really be used if you've used another IPC method and have found it lacking or restrictive.

Rudimentary setup

Server

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

void server_start()
{
    // you can change this to whatever you want
    // but it's good practice to put in /var/run
    const char *socket_path = "/var/run/your_server.socket";

    // setup socket

    struct sockaddr_un local;
    strcpy(local.sun_path, socket_path);
    unlink(local.sun_path);
    local.sun_family = AF_UNIX;

    int listenfd = socket(AF_UNIX, SOCK_STREAM, 0);
    printf("listenfd: %d\n", listenfd);

    // start the server

    int r = -1;
    while(r != 0) {
        r = bind(listenfd, (struct sockaddr*)&local, sizeof(local));
        printf("bind: %d\n", r);
        usleep(200 * 1000);
    }

    int one = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

    // start listening for new connections

    r = -1;
    while(r != 0) {
        r = listen(listenfd, 20);
        printf("listen: %d\n", r);
        usleep(200 * 1000);
    }

    // wait for new connection, and then process it

    int connfd = -1;
    while(true) {
        if(connfd == -1) {
            // wait for new connection
            connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
            printf("new connfd: %d\n", connfd);
        }

        // process incoming data

        char buffer[4096];
        int len = recv(connfd, buffer, sizeof(buffer), 0);
        if(len == 0) {
            printf("connfd %d disconnected!\n", connfd);
            connfd = -1;
            continue;
        } else {
            printf("connfd %d recieved data: %s", connfd, buffer);
            // send some data back (optional)
            const char *response = "got it!\n";
            send(connfd, response, strlen(response) + 1, 0); 
        }
    }
}

You should run server_start() in a background thread, otherwise it will block.

To test, run the command socat - UNIX-CONNECT:/var/run/your_server.socket which will give you a REPL.

Client

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <string.h>

void client_start()
{
    // setup socket
 
    struct sockaddr_un remote;
    remote.sun_family = AF_UNIX;
    strcpy(remote.sun_path, "/var/run/your_server.socket");

    int connfd = socket(AF_UNIX, SOCK_STREAM, 0);
    printf("connfd: %d\n", connfd);

    // connect to server

    int r = -1;
    while(r != 0) {
        r = connect(connfd, (struct sockaddr *)&remote, sizeof(remote));
        printf("connect: %d\n", r);
    }

    // send and receive messages

    const char *message = "why hello there!\n";
    send(connfd, message, strlen(message), 0);

    char buffer[4096];

    int len = recv(connfd, buffer, sizeof(buffer), 0);
    if(len != 0) {
        printf("recieved response: %s\n", buffer);
    }

    close(connfd);
}

Pitfalls of this example

There are a few issues with this example.

First off, the entire server logic is under one thread. That means only one client can connect at a time. If another client tries to connect, it will hang until the first one disconnects. This can be fixed by putting various parts of the server logic under a dispatch_async, or pthread.

The other issue is that it doesn't handle messages that exceed 4096 characters. To mitigate this, it's best to decide on some sort of "separator" string between messages for your protocol (\0 is a good choice), and then just do the processing only once you hit that separator.

Bypassing sandbox with inet sockets

In iOS 10, daemons/processes with the "seatbelt" entitlement (e.g. mediaserverd) don't allow you to create unix domain sockets anymore. :( This is also a problem with the higher-level libraries that use RocketBootstrap.

Luckily for us, there is still a workaround: local internet sockets. The key difference with these is that they're not actual internet sockets. The way these will be set up is that they can only be accessed from localhost, so essentially it poses the same security risk as any other IPC method.

But seriously, don't use this method unless you absolutely have to. Using regular unix domain sockets is much, much better than this approach, since you can tie the socket to a file descriptor instead of a port. There are unlimited potential file descriptors, and only a finite number of available ports. Don't reserve a port unless you absolutely have to.

Code

Basically, all you'd have to change is the socket setup:

Server

// setup socket

int port = 80; // CHANGE THIS!!!!
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
local.sin_port = htons(port);
int listenfd = socket(AF_INET, SOCK_STREAM, 0);

Client

// setup socket

int port = 80; // CHANGE THIS!!!!!
struct sockaddr_in remote;
remote.sin_family = AF_INET;
remote.sin_port = htons(port);
inet_aton("127.0.0.1", &remote.sin_addr);
int connfd = socket(AF_INET, SOCK_STREAM, 0);

Choosing a port

The huge pitfall of this approach is having to pick a port. You'll have to pick one between 1024-65535, and is not in Wikipedia's list of TCP port numbers and also not in the list of reserved ports below. Naturally, there is a very real chance that the port you pick will conflict with another service, so choose wisely. Typically something above 10000 and below 50000 will be unlikely to conflict with anything.

List of reserved ports on jailbroken iOS

  • 787: cycript (I might actually be wrong about this, I just grepped the source for AF_INET and found a match, this commit is also telling)
  • 27724: EQE