Code-Server with Sidecar Auth

背景

去年年底在qcon2020大会上了解到了某团的微服务网格治理方式, 使用Sidecar + Unix Socket实现了服务的解耦, 目前有个同样的类似场景, 只需要鉴权功能, 所以考虑参考这个方式来实现

使用场景同样是在qcon上得到的启发, 外面各大厂都在研究独立在线IDE, 恰好现在有个需求可以尝试使用这种方式实现, 了解了一番后发现实际上都是基于code-server来实现的, 而code-server只提供了最简单的token鉴权功能, 而我们需要实现对接内部账号, 所以正好使用Sidecar来实现鉴权

调研

外部的Sidecar模式是使用Unix Socket做进程间通讯的, 对比起TCP Socket通讯来说, 减少了协议解析, 所以理论上能获得更高的性能

相关资料

UNIX Socket简介

这部分是copy其他资料的, 简单介绍一下UNIX Socket的概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require('http');

// 创建一个 HTTP 服务器
var server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('hello world');
});
// 从'127.0.0.1'和3000端口开始接收连接
server.listen(3000, '127.0.0.1', () => {
console.log('在端口3000启动了服务器');
});
// 从 UNIX 套接字所在路径 path 上监听连接
server.listen('/path/to/socket', () => {
console.log('从socket路径启动了服务器');
});

在上面示例中,使用port端口形式的监听,本质是监听TCP Socket端口。而使用path文件路径的方式,本质上是Unix Domain SocketUnix域套接字)文件。

什么是Socket

在网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个SocketSocket可以被定义描述为两个应用通信通道的端点,一个Socket端点可以用Socket地址(地址 IP、端口、协议组成)来描述。Socket作为一种进程通信机制,操作系统会分配唯一一个Socket标识,这个标识与通讯协议有关(不仅限于TCPUDP)。

Unix Domain Socket

Unix Domain Socket并不是一个实际的协议,它只在同客户机和服务器通信时使用的API,且一台主机与在不同主机间通信时使用相同的API

Unix Domain Socket有以下特点

  • Unix Domain Socket使用的地址通常是一个文件
  • 在同一主机通讯时,传输速率是不同主机间的两倍
  • Unix Domain Socket套接字描述符可以在同一主机不同进程间传递
  • Unix Domain Socket套接字可以向服务器提供用户认证信息
TCP SocketUnix Domain Socket

无论是TCP Socket套接字还是Unix Domain Socket套接字,每个套接字都是唯一的。TCP Socket通过IP和端口描述,而Unix Domain Socket通过文件路径描述。

TCP属于传输层的协议,使用TCP Socket进行通讯时,需要经过传输层TCP/IP协议的解析。

Unix Domain Socket可用于不同进程间的通讯和传递,使用Unix Domain Socket进行通讯时不需要经过传输层,也不需要使用TCP/IP协议。所以,理论上讲Unix Domain Socket具有更好的传输效率。

实现

架构设计

sequenceDiagram participant B as Browser participant A as Node Auth Server participant S as Code Server participant T as Third Party User Service alt 用户访问 B ->> A: 1. 访问服务 A ->> T: 2. 获取用户信息 opt 权限验证失败 T -->> A: 3.1 无权限 A -->> B: 4.1 跳转登录页 end opt 权限验证成功 T -->> A: 3.2 有权限 A ->> S: 4.2 获取数据 S -->> A: 5.2 返回结果 A -->> B: 6.2 返回结果 end end

可以看到用户实际访问的入口是Sidecar组件, 而Code Server服务没有任何改动, 维护起来非常方便

code-server

code-server提供docker化部署方式, 同时也支持socket启动, 只需要手动修改一下启动命令, 将服务监听修改为通过socket, 并禁用auth验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# code-server.Dockerfile
FROM codercom/code-server

RUN sudo apt-get update && sudo apt-get install -y vim

RUN sudo apt-get update && sudo apt-get install -y npm
RUN sudo npm install -g n
RUN sudo n lts
RUN sudo npm install -g npm

COPY --chown=coder:coder .vscode/user-settings.json /home/coder/.local/share/code-server/User/settings.json
COPY --chown=coder:coder .vscode/user-keybindings.json /home/coder/.local/share/code-server/User/keybindings.json

RUN mkdir -p /home/coder/project/

WORKDIR /home/coder/project/

# ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "--auth", "none", "."]
ENTRYPOINT ["/bin/bash", "-c", "/usr/bin/entrypoint.sh --socket /var/run/unix-socket/server.socket --auth none --disable-telemetry --disable-update-check ./"]

auth-sidecar

这个服务比较轻量, 却是实际服务的入口, 将所有需要登录的请求验证一次用户身份, 如果不通过就返回302并跳转登录, 如果通过则通过unix-socket转发给code-server获取真实数据

这里稍微注意下需要对websocket协议同样进行转发处理, 因为code-server里用到了websocket连接, 如果不处理, 就会导致无法使用
websocket协议同样是携带header的, 所以可以直接使用通用的鉴权方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const http = require('http');
const httpProxy = require('http-proxy');
const express = require('express');
const _ = require('lodash');
const fetch = require('node-fetch').default;

const ServerPort = JSON.parse(process.env.PORT || '8080');
const ProxySocketPath = JSON.parse(process.env.PROXY_SOCKET_PATH || `"/var/run/unix-socket/server.socket"`);
const AuthProxyAddr = `...`;

const getUserInfo = async (headers) => {
if (!headers.cookie && !headers['x-token']) {
return null;
}

const userPermissionResponse = await fetch(AuthProxyAddr, {
timeout: 3000,
headers: {
..._.pick(headers, ['cookie', 'origin', 'referer', 'user-agent', 'x-token']),
'content-type': 'application/json',
},
});

if (userPermissionResponse.status != 200) {
console.log(`Auth Error: get user permission info from auth error [${userPermissionResponse.status}]`);
return null;
}

const data = await userPermissionResponse.json();

if (data.code !== 200) {
console.log(`Auth Error: get user permission info from auth error: ${JSON.stringify(data)}`);
return null;
}

return data.data;
};

const validateUser = async (headers) => {
const data = await getUserInfo(headers);
if (!data) {
return false;
}
return true;
};

// create a server
const app = express();
const proxy = httpProxy.createProxyServer({ target: { socketPath: ProxySocketPath }, ws: true }).on('error', (error, req, res) => {
if (!res.destroyed) {
res.statusCode = 500;
res.end(`Server error`);
}
console.log(`Proxy error`, error);
});
const authProxy = httpProxy
.createProxyServer({
target: AuthProxyAddr,
changeOrigin: true,
xfwd: true,
})
.on('error', (error, req, res) => {
if (!res.destroyed) {
res.statusCode = 500;
res.end(`Server error`);
}
console.log(`Proxy error`, error);
});
const server = http.createServer(app);

// Proxy websockets
server.on('upgrade', async (req, socket, head) => {
// console.log('proxying upgrade request', req.url);
if (!(await validateUser(req.headers))) {
socket.write(`HTTP/1.1 401 Unauthorized\r\n\r\n`);
socket.destroy();
return;
}
proxy.ws(req, socket, head);
});

app.route(['/api/auth', '/login', '/logout']).all((req, res) => {
authProxy.web(req, res, {});
});

app.route('/').all(async (req, res) => {
if (!(await validateUser(req.headers))) {
res.redirect(`/login`);
return;
}
proxy.web(req, res, {});
});

app.route('/vscode-remote-resource').all(async (req, res) => {
if (!(await validateUser(req.headers))) {
res.statusCode = 401;
res.end();
return;
}
proxy.web(req, res, {});
});

// serve static content
app.route('*').all((req, res) => {
proxy.web(req, res, {});
});

server.on('error', (error) => {
console.log(`Server error`, error);
});

server.listen(ServerPort, () => {
console.log(`Server listen ${ServerPort}`);
});

k8s启动

其中code-server-nodeproject目录是通过持久化存储挂载进去的, 保证了重启不会丢失数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
application:
containers:
- name: code-server-node
image: code-server-node
command:
- /bin/bash
- '-c'
- /usr/bin/entrypoint.sh --socket /var/run/unix-socket/server.socket --auth none --disable-telemetry --disable-update-check ./
cpu: '2'
reqCPU: '2'
memory: 4Gi
reqMemory: 2Gi
volumeMounts:
- name: unix-socket-dir
mountPath: /var/run/unix-socket/
imagePullPolicy: Always
- name: code-server-auth
image: code-server-auth
cpu: '1'
reqCPU: '1'
memory: 1Gi
reqMemory: 1Gi
volumeMounts:
- name: unix-socket-dir
mountPath: /var/run/unix-socket/
imagePullPolicy: Always
volumes:
- name: unix-socket-dir
emptyDir: {}

小结

开发完成后实测非常好用, 而且是完全无侵入式实现, 对后续的code-server维护/升级等都无压力, 体验非常好

同时经过这次Sidecar尝试后, 发现后续很多类似服务设计方案都可以考虑使用这种模式实现

问题

这里仍然存在的问题是, node-auth往往使用go/c++等实现, 因为作为主要的流量入口, 往往还是会担心node性能不够. 但是此次的需求因为并不是外部访问, 所以使用node来实现方便快捷, 同时也不需要太过于担心入口的性能问题

作者

Mosby

发布于

2021-05-27

许可协议

评论