记录如何用gluetun+litellm搭建身处海外的大模型网关

Posted by Midk9t's Blog on Sunday, February 22, 2026

大家新年快乐🧨🧨🧨适逢春节,终于有点时间更新下博客了(说的跟平时很忙似的)

最近在折腾大模型相关的各种技术,毕竟AI是时代趋势,现在的工作又已经涉及到了,不得不努力更紧一点了,不敢懈怠,现在的技术迭代实在太快了,还没来得及消化新模型的能力,新技术的原理和应用,下一波就接踵而至。

有了大模型的智能加持,其实有很多辅助生活的小应用可以挖掘,我想第一步还是先把之前的刷题小项目用AI重构以下作为出发点为好,毕竟它是一个可以帮助学习,可以提供复利的工具,越早做好越高收益。简单来说,我希望实现一个多Agent的知识学习系统,能够系统化地把知识文章,片段,按照教育科学的理论,用AI自动生成对应的选择题、问答题,然后定期推送给用户让他们回答,来帮助学习相关领域的知识。

自然而然地,便想到可以用Dify这个AI低代码编排平台来负责AI转化选择题的部分。然而我很快便发现,在国内,用不了openrouter的插件,会提示model not supported in your region(openrouter是一个很热门的大模型供应商网关,使用者只要接上它,就能稳定地调用各家厂商的大模型,比如openai、claude乃至国内的deepseek、GLM等)我想是它在安装的时候会去调用claude的模型来验证,而openrouter会把发起的客户端的ip信息也传过去让claude判断是不是合法,于是便失败了。

作为对网络自由有追求的人,当然不可能这么简单就放弃,折腾了几天,衡量了各种方案,最后选择了用nordvpn+litellm来搭建本地侧的AI网关,来实现在家里的服务器连接claude的服务。这篇文章就来记录一下这个实现方案。

Gluetun:容器级VPN方案

首先要解决的问题是,用什么网络方案来绕过Anthropic的地区限制,简单地头脑风暴一下,便可以想到这些方案:

  • 国内AI代理——计费普遍比原生贵0.5倍,好亏
  • 系统级vpn代理——本身就有nordvpn的订阅,使用linux版的nordvpn连接到新加坡即可访问,但是这样整个主机都走这个vpn出去,不太好
  • http/https proxy——搭建https proxy并在dify中指定环境变量,发现好像它不管这个,没成功

最后问了下gemini,由于dify本身是在docker compose上运行的,所以给我推荐了gluetun

不看不知道,一看惊为天人:它可以让docker容器走各种vpn,而不影响宿主机的网络,只需要把对应的容器通过network_mode: “serivce: gluetun”指定使用gluetun的网络栈即可。

然而dify的docker compose有点复杂,涉及到很多组件和几个network,试过改动没成功,于是便决定不直接使用gluetun+dify,而是gluetun+litellm,然后让dify去接litellm的大模型服务

docker-compose.yaml

services:
  gluetun:
    image: qmcgaw/gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=xxx
      - SERVER_COUNTRIES=Singapore
    ports:
      - "4000:4000"
    restart: always

  litellm:
    image: docker.litellm.ai/berriai/litellm:main-stable
    restart: always
    volumes:
     - ./config.yaml:/app/config.yaml
    command:
     - "--config=/app/config.yaml"
    environment:
      DATABASE_URL: "postgresql://llmproxy:dbpassword@localhost:5432/litellm"
      STORE_MODEL_IN_DB: "True" # allows adding models to proxy via UI
    env_file:
      - .env # Load local .env file
    depends_on:
      - db
      - gluetun
    healthcheck:  # Defines the health check configuration for the container
      test:
        - CMD-SHELL
        - python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')"  # Command to execute for health check
      interval: 30s  # Perform health check every 30 seconds
      timeout: 10s   # Health check command times out after 10 seconds
      retries: 3     # Retry up to 3 times if health check fails
      start_period: 40s  # Wait 40 seconds after container start before beginning health checks
    network_mode: "service:gluetun"

  db:
    image: postgres:16
    restart: always
    container_name: litellm_db
    environment:
      POSTGRES_DB: litellm
      POSTGRES_USER: llmproxy
      POSTGRES_PASSWORD: dbpassword
    volumes:
      - postgres_data:/var/lib/postgresql/data # Persists Postgres data across container restarts
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d litellm -U llmproxy"]
      interval: 1s
      timeout: 5s
      retries: 10
    network_mode: "service:gluetun"

volumes:
  postgres_data:
    name: litellm_postgres_data # Named volume for Postgres data persistence

用上面这个docker-compose.yaml,可以搭建起自动接入nordvpn的litellm网关,后面只需要在litellm上配置openrouter的model,便可以绕过地区限制使用上面的claude模型了,因为从claude看来,客户端是在新加坡发起请求的😎

需要注意的是,上面的WIREGUARD_PRIVATE_KEY,需要运行下面这个指令来获得,nordvpn本身没有直接暴露这个密钥:

curl https://api.nordvpn.com/v1/users/services/credentials -u 'token:${your nordvpn key}'

原理

搭建并验证成功后,顺便用AI了解了一下方案背后的技术原理,在这里记录一下,现在有了AI加持,学习起来真的是事半功倍,然而同时正因过于方便,有时候很容易抓不住该学到那里为止——计算机技术本质上由茫茫多层的抽象组成,要知道了解到那一步就ok,也是需要一些衡量。

首先,gluetun集成了各种vpn的技术栈,可以在容器内搭建vpn隧道,而对于nordvpn,它则会使用wireguard来搭建vpn隧道。

Wireguard

wireguard是一个比openvpn更高级的现代vpn技术,它使用udp来封装ip包,整个程序在内核态运行。启动后,它会在宿主机建立一个把发往外部的数据包发送到内核态的wireguard程序的tun设备,一般叫wg0,同时操控路由表,使得默认路由都会经过这个设备进行封装,最后再从宿主机的默认网卡出去,发送到vpn服务器进行转发。

实机探究

现在我们在linux机器上探究下方案下的网络栈具体是怎样的。

首先,我们使用这个docker-compose.yaml来搭建实验环境:

services:
  gluetun:
    image: qmcgaw/gluetun
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=xxx
      - SERVER_COUNTRIES=Singapore
    restart: always

  netshoot:
	  # 集成了各种网络工具的镜像
    image: nicolaka/netshoot
    container_name: netshoot
    # 保持容器运行,以便能随时 exec 进去
    stdin_open: true
    tty: true
    network_mode: "service:gluetun"

运行起来后,进入netshoot容器,我们首先看看接口:

af2486891ad0:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host proto kernel_lo
       valid_lft forever preferred_lft forever
2: eth0@if88: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether de:a6:52:08:d4:7b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.23.0.2/16 brd 172.23.255.255 scope global eth0
       valid_lft forever preferred_lft forever
3: tun0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.5.0.2/32 brd 10.5.0.2 scope global tun0
       valid_lft forever preferred_lft forever
  • eth0@if88是docker搭建用来连接宿主机的接口,docker容器如何和宿主机网络通信,可以参考这篇问答
  • tun0便是gluetun的wireguard搭建的tun设备,它会把数据包发往处于宿主机内核态的wireguard封包程序,这也是为什么会需要映射/dev/net/tun和NET_ADMIN的linux能力

有了tun接口了,接下来的问题便是,容器内的程序如何默认发送数据包到这个设备,而不是eth0呢?这时我们来看一下路由表:

af2486891ad0:~# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.23.0.1      0.0.0.0         UG    0      0        0 eth0
172.23.0.0      *               255.255.0.0     U     0      0        0 eth0

咦?怎么默认还是发往eth0呢?别紧张,这只是main路由表,linux里面可以有若干个路由表,可以通过匹配条件,让不同数据包走不同的路由表去查路由,我们使用ip rule来看看都有什么路由表规则:

af2486891ad0:~# ip rule
0:      from all lookup local
98:     from all to 172.23.0.0/16 lookup main
100:    from 172.23.0.2 lookup 200
101:    not from all fwmark 0xca6c lookup 51820
32766:  from all lookup main
32767:  from all lookup default
  • 首先看到98,可以看到如果是发往docker网络的,则直接走main路由表的默认路由去eth0

  • 然后看100,源ip为172.23.0.2的,也就是eth0的,则走路由表200去查路由,我们看看路由表200的内容:

    af2486891ad0:~# ip route show table 200
    default via 172.23.0.1 dev eth0 proto static
    

    这里还是没看见走tun0的逻辑

  • 然后看101,not from all fwmark 0xca6c lookup 51820,这条规则的意思是,如果没有0xca6c这个标记的流量,则查看51820表,我们来看看它:

    af2486891ad0:~# ip route show table 51820
    default dev tun0 proto static
    

这下看到了指向tun0的默认路由了。然而这里有个问题:rule98是优先于rule100的,为什么流量不会经过rule98的默认路由直接从eth0出去?经过一番查证得出:在容器中发往外部的数据包,在第一次查ip rule的时候,是没有源ip的,也就是没有172.23.0.2,所以rule98不会命中,而是命中后面的rule100。

在经过rule100路由到tun0后,数据包会在wireguard程序中被封装成udp包,同时为流量打上0xca6c的fwmark,并更改目标ip为vpn服务器的地址,然后再次发回到容器的网络协议栈中,最后经过rule98,经过eth0送到宿主机,最终发往vpn服务器,这便是容器内nordvpn发送封装数据包的完整逻辑。


comments powered by Disqus