Gluetun: truy cập host và container khác
Container dùng network namespace của Gluetun không có interface riêng. Hãy publish port trên Gluetun và chỉ mở các subnet cần truy cập ngoài tunnel.
Điều gì xảy ra khi container tham gia network của gluetun
Container đặt network_mode: service:gluetun sẽ không có network interface riêng. Nó tham gia network namespace của gluetun, nên việc publish port và các firewall rule không còn là thuộc tính của container đó mà trở thành thuộc tính của service gluetun. Mọi câu trả lời bên dưới đều bắt nguồn từ một sự thật này.
Network namespace là bản sao riêng của network stack trong kernel: có interface riêng, routing table riêng, firewall rule riêng và listening socket riêng. Docker mặc định cấp cho mỗi container một namespace. Khi bạn ghi network_mode: service:gluetun, Docker bỏ qua bước đó và đặt container mới vào namespace mà gluetun đã sở hữu. Container vẫn giữ filesystem riêng và file /etc/hosts riêng, và file thứ hai sẽ quan trọng ở phần sau.
Bạn có thể xem trực tiếp:
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentLệnh này in container:, tiếp theo là container ID của gluetun; với container thông thường, lệnh sẽ in bridge. Hướng dẫn này tiếp tục từ định tuyến traffic của Docker qua VPN bằng gluetun: tunnel đã hoạt động, nhưng lúc này không gì có thể kết nối đến container.
Publish port trên gluetun, không phải trên application
Nếu giữ block ports: trên service đặt network_mode, Docker sẽ từ chối tạo container:
Error response from daemon: conflicting options: port publishing and the container type network modeLý do rất rõ. Publish một port nghĩa là thêm rule NAT (network address translation) để chuyển tiếp port trên host vào network namespace riêng của container. Container này không có network namespace riêng. Hãy chuyển mapping sang service gluetun. Số port không thay đổi vì application vẫn listen trên port đó trong namespace dùng chung.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereĐặt block expose: trên service phụ thuộc cũng không có tác dụng. Block networks: ở đó sẽ khiến Compose dừng hẳn: Compose báo service khai báo đồng thời network_mode và networks, vốn loại trừ lẫn nhau, rồi từ chối load toàn bộ file.
Một hệ quả sẽ xuất hiện sau đó. Tất cả container trong namespace dùng chung một port space. Vì vậy, hai application cùng mặc định dùng port 8080 sẽ xung đột. Application khởi động sau sẽ fail với lỗi address already in use. Hãy đổi port của một application trong cấu hình riêng của nó, chẳng hạn biến WEBUI_PORT trên image LinuxServer qBittorrent, rồi publish số port mới trên gluetun.
Các container phía sau gluetun kết nối với nhau như thế nào?
Bên trong namespace, chúng đã dùng chung một loopback interface. Container phía sau gluetun kết nối đến container cùng nhóm tại 127.0.0.1:<port> mà không cần Docker network.
Từ bên ngoài namespace, container này không có tên. DNS tích hợp của Docker phân giải tên service thành địa chỉ của service đó trên user-defined network, nhưng container này không có địa chỉ trên bất kỳ network nào. Vì vậy, một container thông thường như Sonarr không kết nối đến torrent client tại http://qbittorrent:8080. Nó kết nối tại http://gluetun:8080, vì socket đang listen trong namespace của gluetun, trên địa chỉ của gluetun. Điều này có thể gây bất ngờ cho những người đã biết Docker Compose network và cách tên service hoạt động và nghĩ rằng cơ chế đặt tên thông thường vẫn được áp dụng. Cách này cũng hoạt động mà không cần publish gì lên host, vì cả hai container đều nằm trên cùng một Compose network.
Hãy kiểm tra DNS trước khi debug bất kỳ vấn đề nào khác. Gluetun chạy resolver riêng và ghi đè /etc/resolv.conf trong container của chính nó, nhưng /etc/resolv.conf là file riêng của từng container, nên file mà gluetun ghi không phải là file mà ứng dụng của bạn đọc.
docker exec qbittorrent cat /etc/resolv.confLàm thế nào để truy cập một service đang chạy trên Docker host?
Dùng host.docker.internal. Lệnh này cần 2 cấu hình ở 2 vị trí khác nhau vì có 2 vấn đề riêng biệt.
Trước hết là tên. /etc/hosts áp dụng cho từng container, vì vậy mục extra_hosts phải nằm trên application container, không phải trên gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway là giá trị đặc biệt mà Docker thay thế bằng địa chỉ nội bộ của chính host. Trên Linux Docker cài trực tiếp, đó là địa chỉ của bridge docker0, thường là 172.17.0.1. Xác nhận giá trị trên VPS bằng ip -4 addr show docker0. Docker Desktop tự phân giải tên này. Vì vậy, các hướng dẫn viết cho laptop thường bỏ qua dòng extra_hosts, nhưng cùng file đó lại lỗi trên server.
Tiếp theo là route. Chỉ thêm tên mới cho container biết phải dùng địa chỉ nào. Packet vẫn đi ra qua default route của gluetun, tức tunnel, và firewall của gluetun sẽ drop packet đó. Triệu chứng là kết nối treo rồi timeout, thay vì bị từ chối ngay. Bị từ chối nghĩa là packet đã đến nơi và có thành phần trả lời không chấp nhận. Timeout nghĩa là packet chưa đến nơi.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32Sau đó kiểm tra service trên host có thực sự listen trên địa chỉ đó hay không. PostgreSQL chỉ bind vào 127.0.0.1 sẽ không thể được truy cập từ bất kỳ container nào, dù có qua tunnel hay không, vì 127.0.0.1 bên trong namespace là loopback của chính namespace đó. Thay vào đó, bind service vào 172.17.0.1: service sẽ nhận kết nối từ các container nhưng vẫn không listen trên public interface. Xác minh bằng ss -lntp | grep 5432 trên host.
FIREWALL_OUTBOUND_SUBNETS thực sự thay đổi gì
Tài liệu gluetun mô tả đây là danh sách các subnet phân tách bằng dấu phẩy mà gluetun và các container dùng chung network stack với nó được phép truy cập, đồng thời lưu ý rằng thiết lập này liên quan đến thay đổi firewall và routing. Cả hai phần đều quan trọng. Gluetun thêm một route cho mỗi subnet được liệt kê thông qua Docker bridge gateway, vì vậy các packet đến những địa chỉ đó sẽ đi qua eth0 thay vì tunnel. Gluetun cũng mở firewall cho các subnet này, vì nếu không, gluetun sẽ drop outbound traffic không hướng đến VPN server.
Ghi giá trị này mà không có khoảng trắng sau dấu phẩy.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32Có hai thuộc tính dễ bị bỏ qua. Đây là thiết lập ở cấp namespace, nên áp dụng cho mọi container phía sau gluetun, không chỉ container bạn đang nghĩ đến. Thiết lập này cũng chỉ áp dụng cho outbound: nó kiểm soát các connection do container khởi tạo. Các connection đi vào một port đã publish sẽ đi theo đường khác và không cần khai báo ở đây.
Truy cập web UI từ một peer Tailscale
Tailscale cấp cho mỗi máy một địa chỉ trong 100.64.0.0/10, dải địa chỉ được dành riêng cho carrier-grade NAT. Trên tailnet cá nhân, tất cả việc này đều không mất phí. Tuy nhiên, giới hạn user và thiết bị của gói miễn phí sẽ quyết định điều đó còn đúng hay không khi người khác cần truy cập cùng các giao diện UI. Vì billing tính theo user thay vì theo máy, chi phí thực tế của một tailnet trả phí phụ thuộc vào số người bạn mời, không phải số container bạn expose cho họ. Hai hướng này cần cách xử lý khác nhau.
Chiều inbound đơn giản hơn. Publish 8080:8080 trên gluetun sẽ bind cổng đó trên mọi địa chỉ của host. Interface tailscale0 của host cũng là một trong các địa chỉ đó, nên peer chỉ cần mở http://<machine-name>:8080 để truy cập container. Gluetun không tham gia vào đường đi này, vì rule NAT của Docker nằm trên host, bên ngoài namespace.
Để UI chỉ có thể truy cập qua tailnet, hãy bind published port vào địa chỉ Tailscale của host thay vì bind vào mọi địa chỉ.
ports:
- "100.101.102.103:8080:8080/tcp"Trên host, dùng tailscale ip -4 để tìm địa chỉ đó. Trong trường hợp này, bind là biện pháp kiểm soát chặt hơn firewall rule, vì cổng hoàn toàn không được mở trên public interface. Nếu muốn truy cập UI bằng tên HTTPS thay vì host và port, tailscale serve có thể đứng trước published port đó. Tuy nhiên, serve và funnel khác nhau ở đối tượng cuối cùng có thể truy cập và chỉ một trong hai giữ UI trong tailnet. Cách này cũng tránh được vấn đề Docker publish port vượt qua ufw trực tiếp.
Chiều outbound mới là nơi FIREWALL_OUTBOUND_SUBNETS được dùng. Nếu container cần gọi đến một peer, hãy thêm địa chỉ của peer đó và ưu tiên dùng /32 cho từng peer thay vì dùng toàn bộ /10. Nếu máy cần gọi nằm trong một private network được truy cập thông qua VPS quảng bá subnet đó vào tailnet, hãy khai báo dải mạng được quảng bá thay vì địa chỉ 100.x riêng của router. Đồng thời xác nhận host đã chấp nhận các route đó. Tên MagicDNS sẽ không resolve được bên trong container, vì container không dùng resolver của host. Do đó, hãy dùng địa chỉ 100.x dạng số hoặc ghim địa chỉ bằng một dòng extra_hosts. Điều tương tự cũng áp dụng khi bạn chạy control server Tailscale riêng bằng Headscale.
Một file compose hoàn chỉnh cho mô hình phổ biến
Một download client chạy sau VPN, hai web UI chỉ nhận kết nối trên tailnet, và một container đọc cơ sở dữ liệu PostgreSQL đang chạy trên host.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:8000:8000/tcp" # gluetun control server, host only
- "100.101.102.103:8080:8080/tcp" # qBittorrent UI, tailnet only
- "100.101.102.103:9696:9696/tcp" # Prowlarr UI, tailnet only
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_CITIES=Amsterdam
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
- TZ=Europe/Amsterdam
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- /srv/downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- PROWLARR__POSTGRES__HOST=host.docker.internal
- PROWLARR__POSTGRES__PORT=5432
- PROWLARR__POSTGRES__USER=prowlarr
- PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
- PROWLARR__POSTGRES__MAINDB=prowlarr-main
- PROWLARR__POSTGRES__LOGDB=prowlarr-log
volumes:
- ./prowlarr:/config
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedHãy đọc file để hiểu mô hình, không tập trung vào tên sản phẩm. Cả hai UI đều được publish trên gluetun và bind vào địa chỉ tailnet của host, nên chỉ nhận kết nối qua Tailscale. extra_hosts chỉ xuất hiện ở Prowlarr, vì Prowlarr là container thực hiện việc phân giải host.docker.internal. FIREWALL_OUTBOUND_SUBNETS khai báo hai địa chỉ đơn: địa chỉ Docker bridge của host để Prowlarr mở kết nối đến database, và một peer trên tailnet.
PostgreSQL cố ý không xuất hiện trong file. Nó chạy trên VPS dưới dạng system service thông thường và lắng nghe trên 172.17.0.1:5432. Đây là cùng một cách phân lớp như arr stack trên Docker Compose, nhưng database được đưa ra ngoài Docker.
Không đưa private key của WireGuard vào file compose. ${WIREGUARD_PRIVATE_KEY} đọc khóa từ một file .env đặt bên cạnh file compose. Đây là mô hình được trình bày trong env file và secret cho Docker Compose. Mệnh đề condition: service_healthy sử dụng healthcheck có sẵn trong image gluetun, nên không container nào khởi động cho đến khi tunnel báo đã hoạt động. Healthcheck của Compose giải thích dạng cấu hình tổng quát.
Publish trên mọi địa chỉ thay vì chỉ trên tailnet
Xóa tiền tố địa chỉ và các port bind trên 0.0.0.0, vì cấu hình này bao gồm public IP của VPS. Chỉ làm vậy khi có firewall do bạn kiểm soát, và hãy đọc ghi chú về ufw ở trên trước.
ports:
- "8080:8080/tcp"Xác nhận tunnel vẫn đang chuyển lưu lượng
Chạy cùng một request 2 lần, một lần từ bên trong namespace và một lần từ host, rồi so sánh kết quả.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgLệnh đầu tiên phải in ra địa chỉ exit của nhà cung cấp VPN. Lệnh thứ hai phải in ra địa chỉ của VPS. Nếu 2 địa chỉ này giống nhau, lưu lượng của container không đi qua tunnel. Khi đó, mọi cách khắc phục trong hướng dẫn này đều không có tác dụng cho đến khi sửa được vấn đề này.
Routing table cho biết lưu lượng nào đi qua tunnel và lưu lượng nào không.
docker run --rm --network=container:gluetun alpine:3.22 ip route showDefault route phải trỏ đến tunnel interface, tun0. Bên dưới, bạn phải thấy một route cho mỗi entry trong FIREWALL_OUTBOUND_SUBNETS, trỏ đến Docker bridge gateway. Bất kỳ route nào khác đi qua eth0 đều là lưu lượng bỏ qua VPN.
Control server của Gluetun báo cùng public IP trên port 8000, tại /v1/publicip/ip. Các phiên bản gần đây yêu cầu bạn cấu hình authentication cho các route của control server, vì vậy hãy thiết lập phần này trước khi phụ thuộc vào nó.
Lỗ hổng do một subnet sai tạo ra
FIREWALL_OUTBOUND_SUBNETS là một lỗ hổng bạn chủ động mở trên firewall, nên kích thước lỗ hổng chính là mức độ rủi ro. Có 4 cách khiến phạm vi này quá rộng:
0.0.0.0/0gửi toàn bộ lưu lượng ra ngoài tunnel. Hai phép kiểm tra IP ở trên sẽ phát hiện lỗi này ngay lần chạy đầu tiên, vì chúng sẽ trả về cùng một địa chỉ.- Phạm vi rộng hơn mục tiêu. Mở
10.0.0.0/8để truy cập một máy tại10.0.1.7cũng mở mọi địa chỉ mà một peer torrent có thể quảng bá trong phạm vi đó. Hãy ghi10.0.1.7/32. - Phạm vi chồng lấn với các địa chỉ của chính tunnel. Tài liệu gluetun cảnh báo rằng trường hợp này khiến gluetun gửi lưu lượng VPN ra ngoài qua bridge, làm hỏng port forwarding. Hãy kiểm tra giá trị
WIREGUARD_ADDRESSEStrước khi mở bất kỳ private range nào. 100.64.0.0/10cho Tailscale. Cách này mở khoảng 4 triệu địa chỉ chỉ để truy cập một peer. Hãy liệt kê các peer cần dùng dưới dạng các mục/32.
Hãy nhớ rằng setting này áp dụng cho toàn bộ namespace. Mở một subnet để indexer truy cập một host service cũng mở cùng subnet đó cho torrent client dùng chung namespace. Hãy chạy lại kiểm tra public IP sau mỗi thay đổi đối với biến này, vì đây là phép kiểm tra duy nhất cho biết thay đổi có thực hiện đúng điều bạn muốn hay không.
Điều gì bị hỏng khi bạn restart gluetun
gluetun sở hữu namespace, nên vòng đời của gluetun cũng là vòng đời của namespace. Nếu khởi động một container phụ thuộc trong khi gluetun đang dừng, thao tác này sẽ fail ngay lập tức:
Error response from daemon: cannot join network of a non running containerRestart gluetun tại chỗ là lỗi khó nhận biết hơn. Các container phụ thuộc vẫn tiếp tục chạy trong khi namespace mà chúng đã attach bị dựng lại bên dưới. Vì vậy docker ps báo mọi thứ vẫn healthy, nhưng không có dịch vụ nào phản hồi. Sau bất kỳ thay đổi nào đối với service gluetun, hãy recreate toàn bộ group thay vì chỉ restart một phần.
docker compose up -d --force-recreateĐiều tương tự cũng áp dụng cho việc cập nhật image. Nếu pull image gluetun mới rồi chỉ recreate service đó, các service còn lại sẽ tiếp tục trỏ đến một namespace không còn tồn tại.
FAQ
Vì sao Docker báo "port publishing and the container type network mode"?
Vì một block ports: vẫn còn trên service đồng thời đặt network_mode: service:gluetun. Việc publish port thêm một NAT rule để chuyển tiếp một port trên host vào network namespace riêng của container, nhưng container ở network mode này không có namespace riêng. Xóa block ports: khỏi service đó và thêm mapping giống hệt vào service gluetun. Số port không đổi vì ứng dụng vẫn listening trên port đó trong namespace dùng chung.
Các container khác truy cập service chạy phía sau gluetun như thế nào?
Các container trong cùng namespace truy cập lẫn nhau qua 127.0.0.1. Các container bên ngoài namespace đó dùng tên service gluetun, vì vậy http://gluetun:8080 hoạt động còn http://qbittorrent:8080 thì không. Container ứng dụng không có địa chỉ trên bất kỳ Docker network nào, nên embedded DNS server không có gì để resolve cho tên của nó. Không cần publish port cho trường hợp này, miễn là cả hai container cùng dùng một Compose network.
Tôi nên điền gì vào FIREWALL_OUTBOUND_SUBNETS?
Chỉ điền các địa chỉ mà container phía sau gluetun phải khởi tạo kết nối đến, với phạm vi cụ thể nhất có thể. Một máy đơn lẻ được viết là /32. Hai entry thường dùng là Docker host tại 172.17.0.1/32 và một /32 cho mỗi Tailscale peer mà bạn kết nối đến. Không bao giờ thêm 0.0.0.0/0 và không bao giờ thêm một range chồng lấn với các địa chỉ tunnel riêng của VPN. Kết nối inbound đến một port đã publish không cần entry ở đây.
Vì sao container không resolve được tên Tailscale MagicDNS của tôi?
MagicDNS hoạt động bằng cách trỏ resolver của host đến DNS server của Tailscale, nhưng container không dùng resolver của host. Nó dùng giá trị trong /etc/resolv.conf của chính nó; khi chạy phía sau gluetun, đó là DNS setup của gluetun. Xác nhận bằng docker exec <container> cat /etc/resolv.conf. Dùng địa chỉ số 100.x của peer hoặc pin tên bằng một entry extra_hosts trên container đó.
Làm thế nào để xác nhận traffic vẫn đi qua VPN?
Chạy một request từ trong namespace và chạy cùng request đó từ host, sau đó so sánh kết quả. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org phải trả về địa chỉ exit của nhà cung cấp VPN, còn curl -s https://api.ipify.org trên VPS phải trả về địa chỉ của VPS. Hai kết quả giống nhau nghĩa là tunnel không chuyển traffic của container. Chạy lại kiểm tra này sau mỗi thay đổi đối với FIREWALL_OUTBOUND_SUBNETS.