Pages

Friday, June 5, 2020

Understanding Docker Socket

While trying to launch a Docker container from another docker container, I came across the docker daemon socket. This article provides an introduction with the docker socket and how it can be used.

Introducing Sockets
The Term socket commonly refers to the IP sockets. These are generally the ones that are bound to a port and address. We send TCP requests to and get responses from.

Another type of socket is a Unix Socket, where sockets are used for the IPC ( Inter process communication) and are called Unix Domain Sockets or UDS. These sockets generally use the local file system for communication while the IP sockets use the network for communication. Linux provides various utilities for checking the sockets and also for talking with sockets. “Ss” is a command utility for checking the socket details like,
[root@ip-172-31-16-91 ec2-user]# ss -s
Total: 257 (kernel 691)
TCP:   9 (estab 2, closed 1, orphaned 0, synrecv 0, timewait 1/0), ports 0

Transport     Total     IP        IPv6
*              691       -         -        
RAW         0         0         0        
UDP          8         4         4        
TCP          8         6         2        
INET            16    10        6        
FRAG          0         0         0    

The ss utility gives a lot of information about the available sockets, currently open and listening sockets etc.

Nc is a utility where we can communicate using a TCP socket. Open 2 terminals and in the first terminal run,
[root@ip-172-31-16-91 ec2-user]# nc -l 3000

This will take the port 3000 by opening a socket and will stay listening and in the second terminal run,
[root@ip-172-31-16-91 ec2-user]# nc localhost 3000

The second terminal waits for the input, when we enter any content the same content will be displayed on the first terminal window too. This way we can communicate on the TCP ports using the nc command.

Introducing Docker daemon sockets
The docker.sock is a unix socket that docker daemon is listening to. This is the main entry point for the Docker API. This socket can also be a TCP socket but for security reasons docker defaults it to the Unix Socket. 

Since this is a Unix socket, they use the local file system for communication. This means unix sockets are faster but they are confined to local communication only. This is how containers talk to docker hosts on the same machine. Docker cli client uses this socket to execute docker commands by default. You can override these settings as well.
The docker daemon can listen for Docker engine API requests via three types of socket, unix, tcp and fd.Docker engine uses this socket to listen to the REST API calls, and the clients use the socket to send API requests to the server. The CLI is one such client. If you observe the docker architecture, we have 3 components mainly, 
 
Whenever we need to create a container, we use the docker cli and run the commands. Docker cli then passes the arguments to the docker engine through the docker REST api for container creation , deletion etc. This is where the Unix socket comes into picture.  Docker creates a unix socket by the name docker.sock under the /var/run. Docker engine listens on this unix socket for all api calls. Docker cli uses this to send api requests to the docker engine.

Curl, A Swill army knife
Curl is a tool available in all linux machines. We can use curl to act as a client and use Docker rest api to perform operations on docker engine. Curl can talk using the Unix socket vie the --unix-socket flag. Since the Docker api is exposed on Rest, we can make use of the Curl commands to send requests over Http. 

Lets get started by running a simple container as below,
[root@ip-172-31-16-91]# docker run -d -p 6379:6379 redis:latest
Once the container is up and running, lets try to grab some details using the Socket file as below,
[root@ip-172-31-16-91 ec2-user]# curl --unix-socket /var/run/docker.sock http://localhost/images/json | jq
  [
  {
    "Containers": -1,
    "Created": 1590711840,
    "Id": "sha256:36304d3b4540c5143673b2cefaba583a0426b57c709b5a35363f96a3510058cd",
    "Labels": null,
    "ParentId": "",
    "RepoDigests": [
      "redis@sha256:ec277acf143340fa338f0b1a9b2f23632335d2096940d8e754474e21476eae32"
    ],
    "RepoTags": [
      "redis:latest"
    ],
    "SharedSize": -1,
    "Size": 104120748,
    "VirtualSize": 104120748
  }
]

With this we can see all the images available in our current machine. This is same as the “docker images” command.
List the running containers using,
[root@ip-172-31-16-91]# curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq
[
  {
    "Id": "56f94a1d4efc95f566c12b743a0f1f8fdc51b4c06ec5262ff1253e9278283412",
    "Names": [
      "/admiring_williamson"
    ],
    "Image": "redis:latest",
    "ImageID": "sha256:36304d3b4540c5143673b2cefaba583a0426b57c709b5a35363f96a3510058cd",
    "Command": "docker-entrypoint.sh redis-server",
    "Created": 1591360805,
    "Ports": [
      {
        "IP": "0.0.0.0",
        "PrivatePort": 6379,
        "PublicPort": 6379,
        "Type": "tcp"
      }
    ],
    "Labels": {},
    "State": "running",
    "Status": "Up 5 minutes",
    "HostConfig": {
      "NetworkMode": "default"
    },
    "NetworkSettings": {
      "Networks": {
        "bridge": {
          "IPAMConfig": null,
          "Links": null,
          "Aliases": null,
          "NetworkID": "ccbed7cd40074c25f6710d7ccd8d9020e03d6788052feaff3681a101a54811f8",
          "EndpointID": "15adb1219880383bf68c833262083202fb135cd45b41da7ceb2a8590726c6456",
          "Gateway": "172.17.0.1",
          "IPAddress": "172.17.0.2",
          "IPPrefixLen": 16,
          "IPv6Gateway": "",
          "GlobalIPv6Address": "",
          "GlobalIPv6PrefixLen": 0,
          "MacAddress": "02:42:ac:11:00:02",
          "DriverOpts": null
        }
      }
    },
    "Mounts": [
      {
        "Type": "volume",
        "Name": "6668a59a3db452a1d463ff117e0a55da54f5b1a587e2a7374cd38d43b64f0eb8",
        "Source": "",
        "Destination": "/data",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
      }
    ]
  }
]

Get details regarding the Running Container : Details about the running container can be obtained using,
[root@ip-172-31-16-91]# curl --unix-socket /var/run/docker.sock http://localhost/images/36304d3b4540/json | jq

Write Operation with the Unix Socket : Write operations can also be performed with the unix socket. Operations like tagging etc can be done by making a rest call to the socket as below,
[root@ip-172-31-16-91 ec2-user]# curl -i -X POST --unix-socket /var/run/docker.sock "http://localhost/images/36304d3b4540/tag?repo=redis&tag=testing"
HTTP/1.1 201 Created
Api-Version: 1.40
Docker-Experimental: false
Ostype: linux
Server: Docker/19.03.6-ce (linux)
Date: Fri, 05 Jun 2020 12:52:36 GMT
Content-Length: 0
In the above command, I'm tagging the redis image with a testing tag. Now once this command is run successfully, we can see the results using “docker images”. 

Stream events 
The /events api allows streaming of events on the docker engine. This can be achieved by using the --no-buffer to the curl command to print the output as events occur. The command looks as,
[root@ip-172-31-16-91]# curl --no-buffer --unix-socket /var/run/docker.sock http://localhost/events
This takes you in the listening mode where it will display event when something happens. Now open a new terminal and run a container run command as below,

[root@ip-172-31-16-91 ec2-user]# docker run -d nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
afb6ec6fdc1c: Already exists
dd3ac8106a0b: Pull complete
8de28bdda69b: Pull complete
a2c431ac2669: Pull complete
e070d03fd1b5: Pull complete
Digest: sha256:883874c218a6c71640579ae54e6952398757ec65702f4c8ba7675655156fcca6
Status: Downloaded newer image for nginx:latest
462a065ae91597289cb3052db7013550b8e23527480253fb7d5fe6bf12fcfc58

Now in the first command prompt under the Curl command we can see the live data stream as below,
{"status":"pull","id":"nginx:latest","Type":"image","Action":"pull","Actor":{"ID":"nginx:latest","Attributes":{"maintainer":"NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e","name":"nginx"}},"scope":"local","time":1591361908,"timeNano":1591361908825825763}
{"status":"create","id":"462a065ae91597289cb3052db7013550b8e23527480253fb7d5fe6bf12fcfc58","from":"nginx","Type":"container","Action":"create","Actor":{"ID":"462a065ae91597289cb3052db7013550b8e23527480253fb7d5fe6bf12fcfc58","Attributes":{"image":"nginx","maintainer":"NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e","name":"ecstatic_dijkstra"}},"scope":"local","time":1591361908,"timeNano":1591361908912415052}
{"Type":"network","Action":"connect","Actor":{"ID":"ccbed7cd40074c25f6710d7ccd8d9020e03d6788052feaff3681a101a54811f8","Attributes":{"container":"462a065ae91597289cb3052db7013550b8e23527480253fb7d5fe6bf12fcfc58","name":"bridge","type":"bridge"}},"scope":"local","time":1591361908,"timeNano":1591361908947748392}
{"status":"start","id":"462a065ae91597289cb3052db7013550b8e23527480253fb7d5fe6bf12fcfc58","from":"nginx","Type":"container","Action":"start","Actor":{"ID":"462a065ae91597289cb3052db7013550b8e23527480253fb7d5fe6bf12fcfc58","Attributes":{"image":"nginx","maintainer":"NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e","name":"ecstatic_dijkstra"}},"scope":"local","time":1591361909,"timeNano":1591361909354445673}

We can see a live stream of data of whatever happens in the docker engine.

Docker socket in Container : If we want to launch a new container from another container, the socket file need to be mounted to the docker container. This increases attack surface so you should be careful if you mount docker socket inside a container there are trusted codes running inside that container otherwise you can simply compromise your host that is running docker daemon,since Docker by default launches all containers as root.

Docker socket has a docker group in most installation so users within that group can run docker commands against docker socket without root permission but actual docker containers still get root permission since docker daemon runs as root effectively (it needs root permission to access namespace and cgroups).

No comments :

Post a Comment