优秀的编程知识分享平台

网站首页 > 技术文章 正文

Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)

nanyue 2025-03-25 16:01:41 技术文章 20 ℃

上一篇: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)

这样咱们就能成功的加载第三方应用了。

最近发表
标签列表