使用unix socket进行本机进程间通讯
前言
最近在研究docker, 实际上docker是一个c/s架构的服务。用户通过docker命令与通过网络连接到dockerd服务然后发送请求, 这样就能操作容器。一般来说, 除了root, 还有docker用户组的用户有权操作容器。这种权限管理是怎么做的呢? 本文来探究这个问题。
unix socket的权限控制
不同于tcp/udp socket, unix socket不是基于ip进行连接的, 而是基于文件进行区分的。这种特殊文件叫做socket file, 如果我们cd到/var/run里面进行ls -al | grep docker.sock
就能看到以下输出:
1 | $ cd /var/run |
只有拥有此文件读写权限的用户才能连接到这个socket, 而docker用户组就有这个特权。有了unix socket这个工具, 就能让有一定权限的用户才能连上某个服务, 做好权限管理, 这种权限管理的思路真的很不错。
unix socket的性能和局限
这种套接字性能极强但不能跨主机, 它不经过内核协议栈, 只是进程间数据的拷贝, 因此较快。陈硕说进程间通讯他只会使用tcp, 理由是unix socket不能跨主机, pipe依赖进程间的父子关系, 且数据是单向的, 要双向通信还得开两对pipe, 这太不方便。
但是unix socket的优势在于易于权限管理, 拥有socket读写权限的用户就能操作本机的容器, 这真的很妙很easy。
tcp版本的unix socket用法
unix socket分为tcp版本的和udp版本的, 这里讨论流式的, 也就是tcp版本的unix socket。
要凭空创建一个unix socket, 在go中这点是有技巧的:
1 | net.DialUnix("unix", &net.UnixAddr{Name: "./mysocket", Net: "unix"}, &net.UnixAddr{Name: "./mysocket", Net: "unix"}) |
其实要创建unix socket, 在c语言里面的表达是创建socket之后bind, bind就会自动创建套接字文件
其次在go语言中, ListenUnix也能创建套接字, 并且可选listner关闭后是否删除socket文件:
1 | listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: fmt.Sprintf("/var/lib/podkit/socket/%d.sock"ContainerID), Net: "unix"}) |
咱们来看看什么时候才会删除socket文件:
1 | SetUnlinkOnClose sets whether the underlying socket file should be removed from the file system when the listener is closed. |
我建议的是对于一个软件, 安装时便创建好socket文件。事实上docker也是这么做的。
接下来服务端能通过net.ListenUnix监听套接字上过来的连接, 客户端如果要连接服务端, 不必使用DialUnix, 直接使用Dial就行:
1 | conn, err := net.Dial("unix", "path_to_your_socket") |
unix套接字的接口及其简单, 无非就是read和write, close read, close write那一套, 和tcp的一模一样。但是net.Dial返回的net.Conn接口没有close read, close write这种半关的接口。如果需要, 可以net.DialUnix, 它返回UnixConn的接口。
udp版本的unix socket用法
这不会有人用吧?我想不到任何理由需要使用这个。这里占个坑, 以后再来填。
使用文件进行权限管理
对于一个程序, 特别是owner是root且具有suid权限位的可执行文件, 如果想要限制只有某些用户才能使用某些功能的话, 用文件的权限进行控制是个很好的思路。在使用这个程序的某些敏感功能时, 可以通过打开某些文件来判断用户是否“够格”。
只要精心设计文件的权限, 在进行敏感操作的时候尝试打开文件测试权限就可以了。如此一来, 我们可以为程序的每个细分功能都设计单独的权限, 达到精确控制权限的目的。