SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor

วิธีตั้งค่าเครือข่ายให้คอนเทนเนอร์สื่อสารผ่าน Gluetun

คอนเทนเนอร์ที่ใช้ network_mode: service:gluetun จะไม่มีอินเทอร์เฟซเครือข่ายเป็นของตัวเอง คุณต้องเผยแพร่พอร์ตผ่าน Gluetun และกำหนดค่า subnets ที่อนุญาตให้เข้าถึงได้เท่านั้น

เกิดอะไรขึ้นเมื่อคอนเทนเนอร์เข้าร่วมเครือข่ายของ gluetun

คอนเทนเนอร์ที่ตั้งค่า network_mode: service:gluetun จะไม่มีอินเทอร์เฟซเครือข่ายเป็นของตัวเอง มันจะเข้าร่วม network namespace ของ gluetun ดังนั้นการเผยแพร่พอร์ต (port publishing) และกฎของไฟร์วอลล์จะไม่ใช่คุณสมบัติของคอนเทนเนอร์นั้นอีกต่อไป แต่จะกลายเป็นคุณสมบัติของบริการ gluetun แทน คำตอบทุกข้อด้านล่างนี้เป็นผลสืบเนื่องมาจากข้อเท็จจริงดังกล่าว

Network namespace คือสำเนาส่วนตัวของ stack เครือข่ายในระดับเคอร์เนล ซึ่งประกอบด้วยอินเทอร์เฟซของตัวเอง ตารางเส้นทาง (routing table) ของตัวเอง กฎไฟร์วอลล์ของตัวเอง และซ็อกเก็ตที่คอยฟัง (listening sockets) ของตัวเอง โดยปกติ Docker จะสร้าง namespace ให้คอนเทนเนอร์ละหนึ่งชุด แต่เมื่อคุณระบุ network_mode: service:gluetun ตัว Docker จะข้ามขั้นตอนนี้ไปและนำคอนเทนเนอร์ใหม่เข้าไปอยู่ใน namespace ที่ gluetun ครอบครองอยู่แล้ว คอนเทนเนอร์จะยังคงมีระบบไฟล์และไฟล์ /etc/hosts เป็นของตัวเอง ซึ่งไฟล์หลังนี้มีความสำคัญในภายหลัง

คุณสามารถตรวจสอบเรื่องนี้ได้โดยตรง

docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent

คำสั่งดังกล่าวจะแสดงผล container: ตามด้วย ID ของคอนเทนเนอร์ gluetun ในขณะที่คอนเทนเนอร์ทั่วไปจะแสดงผลเป็น bridge คู่มือนี้จะอธิบายต่อจากหัวข้อ การส่งทราฟฟิก Docker ผ่าน VPN ด้วย gluetun: เมื่ออุโมงค์เครือข่ายทำงานได้แล้ว แต่ในขณะนี้ไม่มีสิ่งใดสามารถสื่อสารกับคอนเทนเนอร์ได้

เผยแพร่พอร์ตบน gluetun ไม่ใช่บนแอปพลิเคชัน

การทิ้งบล็อก ports: ไว้บนเซอร์วิสที่ตั้งค่า network_mode จะทำให้ Docker ปฏิเสธการสร้างคอนเทนเนอร์:

Error response from daemon: conflicting options: port publishing and the container type network mode

สาเหตุนั้นชัดเจน การเผยแพร่พอร์ตหมายถึงการเพิ่มกฎ NAT (network address translation) เพื่อส่งต่อพอร์ตของโฮสต์เข้าไปยัง network namespace ของคอนเทนเนอร์ แต่คอนเทนเนอร์นี้ไม่มี namespace เป็นของตนเอง ให้ย้ายการแมปพอร์ตไปไว้ที่เซอร์วิส gluetun แทน หมายเลขพอร์ตไม่จำเป็นต้องเปลี่ยนแปลง เนื่องจากแอปพลิเคชันยังคงฟังคำสั่งอยู่ที่พอร์ตเดิมภายใน namespace ที่ใช้ร่วมกัน

services:
  gluetun:
    ports:
      - "8080:8080/tcp"   # qBittorrent web UI

  qbittorrent:
    network_mode: "service:gluetun"
    # no ports: block here

บล็อก expose: บนเซอร์วิสที่เป็นตัวพึ่งพานั้นไม่มีประโยชน์เช่นกัน และบล็อก networks: ในตำแหน่งดังกล่าวจะทำให้เกิดการหยุดทำงานทันที: Compose จะรายงานว่าเซอร์วิสมีการประกาศค่า network_mode และ networks ที่ขัดแย้งกันเอง และปฏิเสธที่จะโหลดไฟล์นั้นโดยสิ้นเชิง

ผลกระทบประการหนึ่งจะปรากฏในภายหลัง คอนเทนเนอร์ทุกตัวใน namespace เดียวกันจะใช้พื้นที่พอร์ตชุดเดียวกัน ดังนั้นหากแอปพลิเคชันสองตัวมีค่าเริ่มต้นเป็น 8080 ทั้งคู่จะเกิดการชนกัน และตัวที่เริ่มทำงานทีหลังจะล้มเหลวพร้อมข้อผิดพลาดว่า address already in use ให้เปลี่ยนค่าของแอปพลิเคชันตัวใดตัวหนึ่งในการตั้งค่าของแอปนั้นเอง เช่น ตัวแปร WEBUI_PORT ในอิมเมจ qBittorrent ของ LinuxServer จากนั้นจึงเผยแพร่หมายเลขพอร์ตใหม่บน gluetun

คอนเทนเนอร์ที่อยู่หลัง gluetun สื่อสารกันเองได้อย่างไร

ภายในเนมสเปซเดียวกัน คอนเทนเนอร์จะใช้ loopback interface ร่วมกันอยู่แล้ว คอนเทนเนอร์ที่อยู่หลัง gluetun จึงสามารถเข้าถึงคอนเทนเนอร์พี่น้องได้ที่ 127.0.0.1:<port> โดยไม่ต้องผ่าน Docker network ใดๆ

จากภายนอกเนมสเปซ คอนเทนเนอร์นี้จะไม่มีชื่อเรียก ระบบ DNS ฝังตัวของ Docker จะแปลงชื่อบริการให้เป็นที่อยู่บนเครือข่ายที่ผู้ใช้กำหนด แต่คอนเทนเนอร์นี้ไม่มีที่อยู่บนเครือข่ายใดเลย ดังนั้นคอนเทนเนอร์ทั่วไปอย่าง Sonarr จึงไม่สามารถเข้าถึงไคลเอนต์ torrent ได้ที่ http://qbittorrent:8080 แต่จะเข้าถึงได้ที่ http://gluetun:8080 เนื่องจากซ็อกเก็ตกำลังรอรับการเชื่อมต่ออยู่ในเนมสเปซของ gluetun และใช้ที่อยู่ของ gluetun สิ่งนี้มักทำให้ผู้ที่เข้าใจ วิธีการทำงานของ Docker Compose networks และ service names รู้สึกแปลกใจ เพราะคาดหวังว่าการตั้งชื่อแบบปกติจะใช้งานได้ นอกจากนี้ยังสามารถทำงานได้โดยไม่ต้องเปิดพอร์ตใดๆ ออกสู่โฮสต์ เนื่องจากคอนเทนเนอร์ทั้งสองอยู่บน Compose network เดียวกัน

ให้ตรวจสอบ DNS ก่อนเริ่มแก้ไขปัญหาอื่นใด gluetun มีตัวแก้ไข DNS ของตัวเองและเขียนทับ /etc/resolv.conf ภายในคอนเทนเนอร์ของมันเอง แต่ /etc/resolv.conf เป็นไฟล์ที่แยกเฉพาะแต่ละคอนเทนเนอร์ ดังนั้นไฟล์ที่ gluetun เขียนขึ้นจึงไม่ใช่ไฟล์ที่แอปพลิเคชันของคุณอ่าน

docker exec qbittorrent cat /etc/resolv.conf

ฉันจะเข้าถึงบริการที่ทำงานอยู่บน Docker host ได้อย่างไร

ให้ใช้ host.docker.internal โดยต้องตั้งค่าสองจุดในตำแหน่งที่ต่างกัน เนื่องจากมีสองส่วนที่ทำงานไม่สัมพันธ์กัน

เริ่มจากชื่อก่อน /etc/hosts จะถูกกำหนดแยกตามคอนเทนเนอร์ ดังนั้นรายการ extra_hosts จึงต้องอยู่ในคอนเทนเนอร์ของแอปพลิเคชัน ไม่ใช่ใน gluetun

  prowlarr:
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"

host-gateway เป็นค่าพิเศษที่ Docker จะแทนที่ด้วยที่อยู่ภายในของตัว host เอง สำหรับการติดตั้ง Docker บน Linux ทั่วไป ค่านี้คือที่อยู่ของ bridge docker0 ซึ่งโดยปกติคือ 172.17.0.1 ให้ยืนยันค่าของคุณด้วยคำสั่ง ip -4 addr show docker0 บน VPS สำหรับ Docker Desktop จะแก้ไขชื่อนี้ได้ด้วยตัวเอง ซึ่งเป็นเหตุผลว่าทำไมคู่มือที่เขียนบนแล็ปท็อปจึงข้ามบรรทัด extra_hosts ไป และทำให้ไฟล์เดียวกันใช้งานไม่ได้เมื่อนำไปใช้บนเซิร์ฟเวอร์

ส่วนถัดมาคือเส้นทาง (route) การเพิ่มชื่อเพียงอย่างเดียวเป็นการบอกคอนเทนเนอร์ว่าต้องใช้ที่อยู่ใด แต่แพ็กเก็ตยังคงถูกส่งออกผ่านเส้นทางเริ่มต้นของ gluetun ซึ่งก็คืออุโมงค์ (tunnel) และไฟร์วอลล์ของ gluetun จะทิ้งแพ็กเก็ตนั้นไป อาการที่พบคือการเชื่อมต่อค้างแล้วหมดเวลา (timeout) แทนที่จะถูกปฏิเสธ (refused) การถูกปฏิเสธหมายความว่าแพ็กเก็ตไปถึงและมีบางอย่างตอบกลับมาว่าไม่ได้ ส่วนการหมดเวลาหมายความว่าแพ็กเก็ตไปไม่ถึงปลายทาง

  gluetun:
    environment:
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32

จากนั้นให้ตรวจสอบว่าบริการบน host กำลังฟัง (listening) อยู่ที่ที่อยู่นั้นจริงหรือไม่ เซิร์ฟเวอร์ PostgreSQL ที่ผูกไว้กับ 127.0.0.1 เท่านั้นจะไม่สามารถเข้าถึงได้จากคอนเทนเนอร์ใดๆ ไม่ว่าจะผ่านอุโมงค์หรือไม่ก็ตาม เพราะ 127.0.0.1 ภายใน namespace คือ loopback ของตัว namespace เอง ให้เปลี่ยนไปผูกกับ 172.17.0.1 แทน ซึ่งจะยอมรับการเชื่อมต่อจากคอนเทนเนอร์โดยที่ยังคงไม่เปิดเผยบนอินเทอร์เฟซสาธารณะ ตรวจสอบความถูกต้องด้วย ss -lntp | grep 5432 บน host

สิ่งที่ FIREWALL_OUTBOUND_SUBNETS เปลี่ยนแปลงจริง

เอกสารของ gluetun อธิบายว่าค่านี้คือรายการ subnet ที่คั่นด้วยเครื่องหมายจุลภาค ซึ่ง gluetun และคอนเทนเนอร์ที่ใช้ network stack ร่วมกันได้รับอนุญาตให้เข้าถึงได้ โดยระบุว่ามีการเปลี่ยนแปลงทั้งในส่วนของ firewall และ routing ทั้งสองส่วนนี้มีความสำคัญเท่ากัน gluetun จะเพิ่มเส้นทาง (route) สำหรับแต่ละ subnet ที่ระบุผ่าน Docker bridge gateway เพื่อให้แพ็กเก็ตสำหรับที่อยู่เหล่านั้นออกทาง eth0 แทนที่จะออกทาง VPN tunnel นอกจากนี้ยังเป็นการเปิด firewall สำหรับ subnet เหล่านั้นด้วย เนื่องจากโดยปกติแล้ว gluetun จะทิ้ง (drop) ทราฟฟิกขาออกที่ไม่มุ่งหน้าไปยัง VPN server

ให้ระบุค่าโดยไม่มีช่องว่างหลังเครื่องหมายจุลภาค

      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32

มีคุณสมบัติสองประการที่มักถูกมองข้าม ประการแรก การตั้งค่านี้เป็นระดับ namespace ดังนั้นจึงมีผลกับทุกคอนเทนเนอร์ที่อยู่เบื้องหลัง gluetun ไม่ใช่แค่คอนเทนเนอร์ที่คุณตั้งใจไว้เท่านั้น ประการที่สอง การตั้งค่านี้มีผลเฉพาะขาออก (outbound) เท่านั้น โดยจะควบคุมการเชื่อมต่อที่คอนเทนเนอร์เป็นผู้เริ่ม ส่วนการเชื่อมต่อที่เข้ามายังพอร์ตที่เปิดไว้ (published port) จะใช้เส้นทางอื่นและไม่จำเป็นต้องระบุในส่วนนี้

การเข้าถึงเว็บ UI จาก Tailscale peer

Tailscale กำหนดที่อยู่ให้กับทุกเครื่องใน 100.64.0.0/10 ซึ่งเป็นช่วงที่สงวนไว้สำหรับ carrier grade NAT การใช้งานนี้ไม่มีค่าใช้จ่ายสำหรับ tailnet ส่วนบุคคล แม้ว่า ขีดจำกัดผู้ใช้ 6 คนของแผนฟรี จะเป็นตัวตัดสินว่าเงื่อนไขนี้ยังคงอยู่หรือไม่เมื่อมีผู้อื่นต้องการเข้าถึง UI เดียวกัน อุปกรณ์สามารถเชื่อมต่อได้ไม่จำกัดในทุกแผน และเนื่องจากการเรียกเก็บเงินนับตามจำนวนผู้ใช้ไม่ใช่จำนวนเครื่อง ค่าใช้จ่ายจริงของ tailnet แบบชำระเงิน จึงขึ้นอยู่กับจำนวนคนที่คุณเชิญ ไม่ใช่จำนวนคอนเทนเนอร์ที่คุณเปิดให้เข้าถึง การเชื่อมต่อทั้งสองทิศทางต้องใช้วิธีการที่แตกต่างกัน

การเชื่อมต่อขาเข้า (Inbound) เป็นส่วนที่ง่ายกว่า การประกาศ 8080:8080 บน gluetun จะทำการ bind พอร์ตนั้นบนทุกที่อยู่ของโฮสต์ และอินเทอร์เฟซ tailscale0 ของโฮสต์ก็เป็นหนึ่งในนั้น ดังนั้น peer จึงสามารถเปิด http://<machine-name>:8080 เพื่อเข้าถึงคอนเทนเนอร์ได้ Gluetun ไม่ได้มีส่วนเกี่ยวข้องในเส้นทางนี้ เนื่องจากกฎ NAT ของ Docker อยู่ที่ระดับโฮสต์ ซึ่งอยู่นอก namespace

หากต้องการให้เข้าถึง UI ได้เฉพาะผ่าน tailnet เท่านั้น ให้ bind พอร์ตที่ประกาศไว้เข้ากับที่อยู่ Tailscale ของโฮสต์แทนที่จะ bind กับทุกที่อยู่

    ports:
      - "100.101.102.103:8080:8080/tcp"

ค้นหาที่อยู่นั้นด้วย tailscale ip -4 บนโฮสต์ การ bind วิธีนี้เป็นการควบคุมที่เข้มงวดกว่ากฎ firewall เพราะพอร์ตจะไม่ถูกเปิดบนอินเทอร์เฟซสาธารณะเลย หากคุณต้องการเข้าถึง UI ผ่านชื่อ HTTPS แทนการใช้โฮสต์และพอร์ต tailscale serve สามารถทำหน้าที่เป็นหน้าด่านให้กับพอร์ตที่ประกาศไว้นั้นได้ แม้ว่า serve และ funnel จะมีความแตกต่างกันในแง่ของผู้ที่สามารถเข้าถึงได้ และมีเพียงวิธีเดียวเท่านั้นที่คง UI ไว้บน tailnet นอกจากนี้ยังช่วยเลี่ยงปัญหาใน การที่ Docker ประกาศพอร์ตโดยข้าม ufw รูปแบบ Tailscale sidecar สำหรับ Compose นั้นทำได้มากกว่าโดยการตัดพอร์ตโฮสต์ออกไปเลย แต่วิธีนี้ทำงานโดยการนำแอปไปไว้ใน network namespace ของ sidecar ซึ่งคอนเทนเนอร์ที่อยู่หลัง gluetun ได้ใช้ network_mode ไปกับ gluetun แล้ว

การเชื่อมต่อขาออก (Outbound) คือจุดที่ FIREWALL_OUTBOUND_SUBNETS จะกลับมามีบทบาท หากคอนเทนเนอร์จำเป็นต้องเรียกใช้ peer ให้เพิ่มที่อยู่ของ peer นั้น และควรเลือกใช้ /32 แยกตามราย peer แทนการใช้ /10 ทั้งหมด หากเครื่องที่คุณกำลังเรียกใช้งานอยู่ในเครือข่ายส่วนตัวที่เข้าถึงได้ผ่าน VPS ที่ประกาศ subnet นั้นไปยัง tailnet ของคุณ ให้ระบุช่วงที่ประกาศไว้แทนที่ที่อยู่ 100.x ของตัว router เอง และตรวจสอบว่าโฮสต์ได้ยอมรับเส้นทางเหล่านั้นแล้ว ชื่อ MagicDNS จะไม่สามารถ resolve ภายในคอนเทนเนอร์ได้ เนื่องจากคอนเทนเนอร์ไม่ได้ใช้ resolver ของโฮสต์ ดังนั้นให้ใช้ที่อยู่ 100.x แบบตัวเลข หรือกำหนดค่าด้วยบรรทัด extra_hosts วิธีการเดียวกันนี้ยังใช้ได้เมื่อคุณรัน Tailscale control server ของคุณเองด้วย Headscale

ไฟล์ compose ฉบับสมบูรณ์สำหรับโครงสร้างทั่วไป

ไคลเอนต์ดาวน์โหลดที่อยู่หลัง VPN, เว็บ UI สองรายการที่ตอบสนองเฉพาะบน tailnet และคอนเทนเนอร์หนึ่งรายการที่อ่านฐานข้อมูล PostgreSQL ซึ่งรันอยู่บนโฮสต์

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-stopped

ให้ศึกษาไฟล์นี้เพื่อดูรูปแบบการตั้งค่ามากกว่าชื่อผลิตภัณฑ์ UI ทั้งสองรายการถูกเผยแพร่ผ่าน gluetun และผูกไว้กับที่อยู่ tailnet ของโฮสต์ ดังนั้นจึงตอบสนองผ่าน Tailscale เท่านั้นและไม่ตอบสนองจากที่อื่น มีเพียง Prowlarr เท่านั้นที่มีบรรทัด extra_hosts เนื่องจาก Prowlarr เป็นคอนเทนเนอร์ที่ทำหน้าที่ resolve host.docker.internal ส่วน FIREWALL_OUTBOUND_SUBNETS ระบุที่อยู่เดี่ยวสองรายการ ได้แก่ ที่อยู่ Docker bridge ของโฮสต์เพื่อให้ Prowlarr สามารถเปิดการเชื่อมต่อฐานข้อมูลได้ และที่อยู่ของ peer ใน tailnet อีกหนึ่งรายการ

เซิร์ฟเวอร์ PostgreSQL ถูกละไว้จากไฟล์นี้โดยเจตนา โดยรันบน VPS ในฐานะ system service ปกติที่ฟังการเชื่อมต่อบน 172.17.0.1:5432 ซึ่งเป็นการจัดลำดับชั้นแบบเดียวกับ arr stack บน Docker Compose โดยย้ายฐานข้อมูลออกมาไว้นอก Docker

ควรเก็บ WireGuard private key ไว้นอกไฟล์ compose โดย ${WIREGUARD_PRIVATE_KEY} จะอ่านค่าจากไฟล์ .env ที่วางอยู่ข้างกัน ซึ่งเป็นรูปแบบที่อธิบายไว้ใน ไฟล์ env และ secrets สำหรับ Docker Compose ส่วนคำสั่ง condition: service_healthy ใช้ healthcheck ที่มาพร้อมกับอิมเมจ gluetun อยู่แล้ว เพื่อให้มั่นใจว่าไม่มีบริการใดเริ่มทำงานจนกว่า tunnel จะรายงานสถานะว่าพร้อมใช้งาน Compose healthchecks อธิบายถึงรูปแบบการใช้งานทั่วไปในเรื่องนี้

การเผยแพร่บนทุกที่อยู่แทนที่จะเป็นเฉพาะ tailnet

ให้ลบ prefix ของที่อยู่และการผูกพอร์ตบน 0.0.0.0 ออก ซึ่งรวมถึง IP สาธารณะของ VPS ด้วย ให้ดำเนินการนี้เฉพาะเมื่ออยู่หลังไฟร์วอลล์ที่คุณควบคุมได้เท่านั้น และโปรดอ่านหมายเหตุเกี่ยวกับ ufw ด้านบนก่อน

    ports:
      - "8080:8080/tcp"

ยืนยันว่าอุโมงค์ยังคงรับส่งข้อมูล

ให้รันคำขอเดิมสองครั้ง ครั้งแรกจากภายใน namespace และครั้งที่สองจาก host จากนั้นนำมาเปรียบเทียบกัน

docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.org

ครั้งแรกควรแสดงที่อยู่ขาออกของผู้ให้บริการ VPN ของคุณ ส่วนครั้งที่สองควรแสดงที่อยู่ของ VPS หากที่อยู่ตรงกัน แสดงว่าข้อมูลของ container ไม่ได้ผ่านอุโมงค์ และการแก้ไขทุกอย่างในคู่มือนี้จะไม่มีความหมายจนกว่าจะแก้ไขจุดนี้ให้ถูกต้อง

ตาราง routing จะแสดงให้เห็นว่าข้อมูลใดผ่านอุโมงค์และข้อมูลใดไม่ผ่าน

docker run --rm --network=container:gluetun alpine:3.22 ip route show

เส้นทาง default route ควรชี้ไปที่อินเทอร์เฟซของอุโมงค์คือ tun0 ด้านล่างนั้นคุณควรเห็นหนึ่งเส้นทางต่อหนึ่งรายการใน FIREWALL_OUTBOUND_SUBNETS ซึ่งชี้ไปที่เกตเวย์ของ Docker bridge เส้นทางอื่นใดที่ออกผ่าน eth0 คือข้อมูลที่ข้าม VPN ไป

Control server ของ Gluetun สามารถรายงาน IP สาธารณะเดียวกันได้ที่พอร์ต 8000 ผ่าน /v1/publicip/ip แต่จะทำได้ก็ต่อเมื่อคุณตั้งค่าการยืนยันตัวตนแล้วเท่านั้น เอกสารของ Gluetun ระบุว่าเส้นทางของ control server ทั้งหมดถูกตั้งค่าเป็นส่วนตัวโดยค่าเริ่มต้นและไม่มีรายการใดเข้าถึงได้จากสาธารณะ ดังนั้นคำขอทั่วไปไปยัง path ดังกล่าวจึงล้มเหลว ให้กำหนดค่าด้วยไฟล์ยืนยันตัวตนที่ bind mount ไว้ที่ /gluetun/auth/config.toml โดยใช้วิธี basic หรือ apikey จากนั้นรีสตาร์ท Gluetun ไฟล์ compose ด้านบนได้ mount ./gluetun ไว้ที่ /gluetun เรียบร้อยแล้ว ดังนั้นไฟล์ดังกล่าวจึงต้องวางไว้ที่ ./gluetun/auth/config.toml บน host ตัวแปร HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE เป็นวิธีที่รวดเร็วกว่า และเอกสารแนะนำอย่างยิ่งว่าไม่ควรตั้งค่าเป็น none สำหรับรุ่นที่เก่ากว่า v3.39.1 เซิร์ฟเวอร์จะไม่มีการล็อกแบบนี้ และเอกสารเตือนให้คุณระมัดระวังสิ่งที่สามารถเข้าถึงเซิร์ฟเวอร์ได้

ผลกระทบจากการกำหนด subnet ที่ผิดพลาด

FIREWALL_OUTBOUND_SUBNETS คือช่องโหว่ที่คุณจงใจเปิดไว้บนไฟร์วอลล์ ดังนั้นขนาดของช่องโหว่จึงเท่ากับระดับความเสี่ยง นี่คือ 4 วิธีที่ทำให้ช่องโหว่ดังกล่าวใหญ่เกินความจำเป็น:

  • 0.0.0.0/0 ส่งทุกอย่างออกไปนอกอุโมงค์ การตรวจสอบ IP สองรายการข้างต้นจะตรวจพบปัญหานี้ในการรันครั้งแรก เพราะจะส่งคืนค่าที่อยู่เดียวกัน
  • การกำหนดช่วงที่กว้างกว่าเป้าหมาย การเปิด 10.0.0.0/8 เพื่อเข้าถึงเครื่องเดียวที่ 10.0.1.7 จะเป็นการเปิดทุกที่อยู่ที่ peer ของ torrent อาจประกาศเข้ามาในช่วงนั้นด้วย ให้ระบุเป็น 10.0.1.7/32 แทน
  • ช่วงที่ทับซ้อนกับที่อยู่ของอุโมงค์เอง เอกสารของ gluetun เตือนว่าการทำเช่นนี้จะทำให้ gluetun ส่ง traffic ของ VPN ออกไปทาง bridge แทน ซึ่งจะทำให้การทำ port forwarding ใช้งานไม่ได้ ตรวจสอบค่า WIREGUARD_ADDRESSES ของคุณก่อนเปิดช่วง private range ใดๆ
  • 100.64.0.0/10 สำหรับ Tailscale ซึ่งเป็นการเปิดที่อยู่ถึงประมาณสี่ล้านหมายเลขเพื่อให้เข้าถึง peer ได้เพียงเครื่องเดียว ให้ระบุรายการ peer ที่คุณต้องการเป็นรายการ /32 แทน

โปรดจำไว้ว่าการตั้งค่านี้ครอบคลุมทั้ง namespace การเปิด subnet เพื่อให้ indexer เข้าถึง host service ได้ จะเป็นการเปิด subnet เดียวกันนั้นให้กับ torrent client ที่แชร์ namespace เดียวกันด้วย ให้รันการตรวจสอบ public IP ซ้ำทุกครั้งหลังเปลี่ยนค่าตัวแปรนี้ เพราะเป็นวิธีทดสอบเดียวที่จะยืนยันได้ว่าการเปลี่ยนแปลงนั้นส่งผลตามที่คุณต้องการจริงหรือไม่

สิ่งที่เกิดขึ้นเมื่อรีสตาร์ท gluetun

เนื่องจาก gluetun เป็นผู้ควบคุม namespace วงจรชีวิตของ gluetun จึงเท่ากับวงจรชีวิตของ namespace นั้น การเริ่ม container ที่พึ่งพา gluetun ในขณะที่ gluetun ยังไม่ทำงานจะทำให้เกิดความล้มเหลวในทันที:

Error response from daemon: cannot join network of a non running container

การรีสตาร์ท gluetun โดยตรงเป็นความล้มเหลวที่ตรวจพบได้ยากกว่า container ที่พึ่งพา gluetun จะยังคงทำงานอยู่ต่อไปในขณะที่ namespace ที่พวกมันเชื่อมต่ออยู่ถูกสร้างขึ้นใหม่ภายใต้ตัวมันเอง ส่งผลให้ docker ps รายงานว่าทุกอย่างปกติ แต่ไม่มีบริการใดตอบสนองได้ หลังจากมีการเปลี่ยนแปลงใดๆ กับ service ของ gluetun ให้สร้างกลุ่ม container ขึ้นมาใหม่ทั้งหมดแทนการรีสตาร์ทเพียงส่วนใดส่วนหนึ่ง

docker compose up -d --force-recreate

กรณีเดียวกันนี้ใช้กับการอัปเดต image การดึง image ของ gluetun เวอร์ชันใหม่มาใช้และสร้างเฉพาะ service นั้นขึ้นมาใหม่ จะทำให้ container อื่นๆ อ้างอิงไปยัง namespace ที่ไม่มีอยู่จริงอีกต่อไป

FAQ

ทำไม Docker ถึงแจ้งเตือนเรื่อง "port publishing and the container type network mode"?

เพราะมีการระบุบล็อก ports: ไว้ในบริการที่ตั้งค่า network_mode: service:gluetun เอาไว้ด้วย การเปิดพอร์ต (publishing) จะเพิ่มกฎ NAT เพื่อส่งต่อพอร์ตจากโฮสต์เข้าไปยัง network namespace ของคอนเทนเนอร์ แต่คอนเทนเนอร์ที่ใช้โหมดนี้จะไม่มี namespace เป็นของตัวเอง ให้ลบบล็อก ports: ออกจากบริการนั้น แล้วย้ายการแมปพอร์ตไปไว้ที่บริการ gluetun แทน โดยใช้หมายเลขพอร์ตเดิมได้เลย เนื่องจากแอปพลิเคชันยังคงฟังพอร์ตนั้นอยู่ภายใน namespace ที่ใช้ร่วมกัน

คอนเทนเนอร์อื่นจะเข้าถึงบริการที่อยู่หลัง gluetun ได้อย่างไร?

คอนเทนเนอร์ที่อยู่ใน namespace เดียวกันจะเข้าถึงกันได้ผ่าน 127.0.0.1 ส่วนคอนเทนเนอร์ที่อยู่ภายนอกให้ใช้ชื่อบริการ gluetun ดังนั้น http://gluetun:8080 จึงใช้งานได้ในขณะที่ http://qbittorrent:8080 ใช้งานไม่ได้ เนื่องจากคอนเทนเนอร์ของแอปพลิเคชันไม่มีที่อยู่บนเครือข่าย Docker ใดๆ DNS server ภายในจึงไม่สามารถหาค่าเพื่อแปลงชื่อได้ ไม่จำเป็นต้องเปิดพอร์ตสำหรับกรณีนี้ ตราบใดที่คอนเทนเนอร์ทั้งสองอยู่ใน Compose network เดียวกัน

ควรใส่ค่าอะไรใน FIREWALL_OUTBOUND_SUBNETS?

ให้ใส่เฉพาะที่อยู่ที่คอนเทนเนอร์หลัง gluetun จำเป็นต้องเชื่อมต่อออกไปเท่านั้น โดยระบุให้แคบที่สุดเท่าที่จะทำได้ เครื่องเดียวให้ระบุเป็น /32 รายการที่พบบ่อยคือ Docker host ที่ 172.17.0.1/32 และ /32 สำหรับ Tailscale peer แต่ละเครื่องที่คุณเรียกใช้ ห้ามใส่ 0.0.0.0/0 และห้ามใส่ช่วง IP ที่ทับซ้อนกับที่อยู่ tunnel ของ VPN ของคุณเอง การเชื่อมต่อขาเข้าผ่านพอร์ตที่เปิดไว้ไม่จำเป็นต้องระบุในส่วนนี้

ทำไมคอนเทนเนอร์ถึงไม่สามารถแปลงชื่อ Tailscale MagicDNS ได้?

MagicDNS ทำงานโดยชี้ตัวแปลงชื่อ (resolver) ของโฮสต์ไปที่ DNS server ของ Tailscale แต่คอนเทนเนอร์ไม่ได้ใช้ resolver ของโฮสต์ มันจะใช้ค่าตามที่ระบุใน /etc/resolv.conf ของตัวเอง ซึ่งเมื่ออยู่หลัง gluetun จะเป็นการตั้งค่า DNS ของ gluetun ให้ตรวจสอบด้วย docker exec <container> cat /etc/resolv.conf โดยให้ใช้ที่อยู่ 100.x แบบตัวเลขของ peer หรือกำหนดชื่อด้วยรายการ extra_hosts ในคอนเทนเนอร์นั้นๆ

จะยืนยันได้อย่างไรว่าทราฟฟิกยังคงวิ่งผ่าน VPN?

ให้รันคำสั่งร้องขอข้อมูลจากภายใน namespace และรันคำสั่งเดียวกันจากโฮสต์ จากนั้นเปรียบเทียบผลลัพธ์ docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org ควรแสดงที่อยู่ขาออกของผู้ให้บริการ VPN ของคุณ ในขณะที่ curl -s https://api.ipify.org บน VPS จะต้องแสดงที่อยู่ของ VPS หากผลลัพธ์ทั้งสองเหมือนกัน แสดงว่า tunnel ไม่ได้นำส่งทราฟฟิกของคอนเทนเนอร์ ให้ทำการตรวจสอบนี้อีกครั้งหลังจากการเปลี่ยนแปลง FIREWALL_OUTBOUND_SUBNETS ทุกครั้ง