上一篇:Dify工具使用全场景:dify-web修改编译指南(源码解读篇·第1期)
我的场景:
最近在使用dify的代码节点执行代码时,发现如果想引用第三方的库会报错,目前代码执行只支持python3和javascript脚本,而python3里有很多好用的python依赖库,如果想引用就得修改源码,不然执行代码会报错。
开源地址:
https://github.com/langgenius/dify
首先来看一下,dify在容器里启动一共有几个服务,如下图:
今天来聊聊dify里的沙盒
源码github地址:
https://github.com/langgenius/dify-sandbox
关于沙箱,在dify里主要在代码执行运行在安全的沙盒中,如下图所示:
官方解释
Dify-Sandbox提供了一种在安全环境中运行不受信任代码的简单方法。它被设计用于多租户环境,其中多个用户可以提交要执行的代码。代码在沙盒环境中执行,这限制了代码可以访问的资源和系统调用。
DifySandbox目前只支持Linux,因为它是为docker容器设计的。它需要以下依赖项:
- libseccomp
- pkg-config
- gcc
- golang
安装步骤
克隆 git clone https://github.com/langgenius/dify-sandbox
运行 ./install.sh 以安装必要的依赖项
运行 ./build/build_[amd64|arm64].sh 构建沙箱二进制文件
运行 ./main 以启动服务器
如果要调试服务器,请首先使用 build script 构建沙盒库二进制文件,然后使用 IDE 根据需要进行调试。
dify沙箱的工作流程
如何在沙箱里跑咱们自己定义的包?
本次咱们以pandas的引入依赖为例子。
首先介绍一下pandas是什么?它是一个基于python的强大数据分析和处理工具库,主要用于数据清洗、数据操作和数据分析。它以直观和高效的方式处理结构化数据,它尤其擅长处理二维数据。
处理流程
我们先进入已运行的docker,先看一下dify-sandbox容器里的结果。
可以看到,容器里关键的两个包为:dependencies 和 conf 包,因此,可以看了,只需要映射这两个包就行了。在docker-compose.yaml中可以如下配置:
因此,我们修改宿主机里的dependencies下的源文件,增加需要支持的依赖,如下图所示:
当重启docker-compose up -d 重启后就会加载进去。如下图:
启动时,会自动拉取依赖。
原型
为什么sandbox启动时会被安装。
主要看一下源码中的初始化依赖的方法,会在沙盒启动时,读取配置文件进行安装,源码如下:
func Run() {
// 初始化配置
initConfig()
// 初始化依赖
go initDependencies()
// 初始化服务
initServer()
}
位置在:
https://github.com/langgenius/dify-sandbox/blob/main/internal/server/server.go
主要的依赖:
func initDependencies() {
log.Info("installing python dependencies...")
dependencies := static.GetRunnerDependencies()
err := python.InstallDependencies(dependencies.PythonRequirements)
if err != nil {
log.Panic("failed to install python dependencies: %v", err)
}
log.Info("python dependencies installed")
log.Info("initializing python dependencies sandbox...")
err = python.PreparePythonDependenciesEnv()
if err != nil {
log.Panic("failed to initialize python dependencies sandbox: %v", err)
}
log.Info("python dependencies sandbox initialized")
// start a ticker to update python dependencies to keep the sandbox up-to-date
go func() {
updateInterval := static.GetDifySandboxGlobalConfigurations().PythonDepsUpdateInterval
tickerDuration, err := time.ParseDuration(updateInterval)
if err != nil {
log.Error("failed to parse python dependencies update interval, skip periodic updates: %v", err)
return
}
ticker := time.NewTicker(tickerDuration)
for range ticker.C {
if err:=updatePythonDependencies(dependencies);err!=nil{
log.Error("Failed to update Python dependencies: %v", err)
}
}
}()
}
func updatePythonDependencies(dependencies static.RunnerDependencies) error {
log.Info("Updating Python dependencies...")
if err := python.InstallDependencies(dependencies.PythonRequirements); err != nil {
log.Error("Failed to install Python dependencies: %v", err)
return err
}
if err := python.PreparePythonDependenciesEnv(); err != nil {
log.Error("Failed to prepare Python dependencies environment: %v", err)
return err
}
log.Info("Python dependencies updated successfully.")
return nil
}
在初始化依赖时,会运行一个执行安装的命令。
包安装后可以运行试试
这里我们会看到一个异常,“error: operation not permitted”异常的意思就是没有权限去执行操作。
从源码中可以看出,其实dify-sandbox是由一种叫做seccomp的技术来完成的,如下图:
这里又得说说什么是seccomp
Secomp(全称:Secure Computing Mode)是Linux内核的一种安全机制,旨在限制进程能够调用的系统调用(syscall)。通过限制系统调用的范围,可以显著降低攻击面,即使程序存在漏洞,攻击者也很难通过恶意输入或代码执行来利用危险的系统调用。
再看看源码,那默认沙盒能调用的系统调用都有哪些?
如图所示:
可以看到:系统调用(System Call)是操作系统提供给应用程序的一组接口,用于请求操作系统内核执行特定的底层操作。它是用户程序和操作系统内核之间的桥梁,使应用程序可以利用操作系统的功能,如文件管理、内存分配、网络通信等。沙盒通过控制这些调用来控制安全的。
再看源码:
设置为ALLOWED_SYSCALLS这个环境变量,然后再去执行python code
沙盒的具体工作原理流程:
code节点->dify沙盒服务->在tmp目录生成python代码->设置环境变量ALLOWED_SYSCALL并执行代码->执行tmp目录生成的python代码->返回结果
其实它执行时,会执行一个python.so的模板文件,如下图:
那这个命令如何来的,我们再看看编译命令:
这里其实调用那个lib下面的python的main.go这个文件,这个文件初始化了一个seccomp,这样就和前面的知识对应起来了。
具体的使用,大家可以跟踪一下代码,好好学习一下。
官方是使用go语言去编译的,我学得太麻烦,还是用容器的方式把修改的内容映射到容器里就行,主要修改两个文件:
conf/config.yaml
app:
port: 8194
debug: True
key: dify-sandbox
max_workers: 4
max_requests: 50
worker_timeout: 5
python_path: /usr/local/bin/python3
enable_network: True # please make sure there is no network risk in your environment
allowed_syscalls: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,28,30,31,32,33,34,35,36,37,39,56,57,60,61,62,63,72,73,80,81,85,86,90,91,105,106,131,186,202,204,217,231,233,234,237,257,262,273,281,291,318,334,435,499,318,334,307,262,16,8,217,1,3,257,0,202,9,12,10,11,15,25,105,106,102,39,110,186,60,231,234,13,16,24,273,274,334,228,96,35,291,233,230,270,201,14,131,318,56,258,83,41,42,49,50,43,44,45,51,47,52,54,271,63,46,307,55,5,72,138,7,281] # please leave it empty if you have no idea how seccomp works
proxy:
socks5: ''
http: ''
https: ''
dependencies/python-requirements.txt
pandas==2.2.3
如果想在容器里运行,可以写个测试文件映射到容器里,测试一下即可,如:
test.py
import ctypes
import json
import os
import sys
import traceback
# setup sys.excepthook
def excepthook(type, value, tb):
sys.stderr.write("".join(traceback.format_exception(type, value, tb)))
sys.stderr.flush()
sys.exit(-1)
sys.excepthook = excepthook
lib = ctypes.CDLL("/var/sandbox/sandbox-python/python.so")
print(lib)
lib.DifySeccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
lib.DifySeccomp.restype = None
os.chdir("/var/sandbox/sandbox-python")
lib.DifySeccomp(65537, 1001, 1)
# declare main function here
# 测试代码可以写在此处
import pandas as pd
# 此处main 方法不要带具体参数
def main() -> dict:
s = pd.Series([1, 3, 5, 6, 8])
return {
"result": "test",
}
from base64 import b64decode
from json import dumps, loads
# execute main function, and return the result
# inputs is a dict, and it
inputs = b64decode("e30=").decode("utf-8")
output = main(**json.loads(inputs))
# convert output to json and print
output = dumps(output, indent=4)
result = f"""<>
{output}
<>"""
print(result)
用test.sh来调用
import ctypes
import json
import os
import sys
import traceback
# setup sys.excepthook
def excepthook(type, value, tb):
sys.stderr.write("".join(traceback.format_exception(type, value, tb)))
sys.stderr.flush()
sys.exit(-1)
sys.excepthook = excepthook
lib = ctypes.CDLL("/var/sandbox/sandbox-python/python.so")
print(lib)
lib.DifySeccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
lib.DifySeccomp.restype = None
os.chdir("/var/sandbox/sandbox-python")
lib.DifySeccomp(65537, 1001, 1)
# declare main function here
# 测试代码可以写在此处
import pandas as pd
# 此处main 方法不要带具体参数
def main() -> dict:
s = pd.Series([1, 3, 5, 6, 8])
return {
"result": "test",
}
from base64 import b64decode
from json import dumps, loads
# execute main function, and return the result
# inputs is a dict, and it
inputs = b64decode("e30=").decode("utf-8")
output = main(**json.loads(inputs))
# convert output to json and print
output = dumps(output, indent=4)
result = f"""<>
{output}
<>"""
print(result)
这样咱们就能成功的加载第三方应用了。