SiamCafe · Blog
Tailscale Mesh Testing Strategy QA
บทความ

Tailscale Mesh Testing Strategy QA

เผยแพร่ 28 พฤษภาคม 2569

Tailscale Mesh Testing Strategy QA คืออะไร

Tailscale เป็น mesh VPN ที่ใช้ WireGuard protocol สร้าง private network ระหว่างอุปกรณ์โดยไม่ต้อง configure firewall หรือ port forwarding ทำงานผ่าน coordination server + peer-to-peer connections Testing Strategy QA คือกลยุทธ์การทดสอบเพื่อรับรองคุณภาพ ครอบคลุม unit tests, integration tests, end-to-end tests และ security tests การรวมสองแนวคิดนี้ช่วยให้ Tailscale mesh deployments มีความน่าเชื่อถือสูง ตรวจจับปัญหา connectivity และ security ก่อน production

Tailscale Architecture

# tailscale_arch.py — Tailscale mesh architecture
import json

class TailscaleArchitecture:
    CONCEPTS = {
        "tailnet": {
            "name": "Tailnet",
            "description": "Private mesh network ที่เชื่อม devices ทั้งหมด — ทุก device เห็นกัน",
            "addressing": "100.x.y.z (CGNAT range) — unique per device",
        },
        "wireguard": {
            "name": "WireGuard Protocol",
            "description": "Modern VPN protocol ที่เร็วและปลอดภัย — kernel-level encryption",
            "benefit": "Low latency, small codebase, proven cryptography",
        },
        "derp": {
            "name": "DERP (Designated Encrypted Relay for Packets)",
            "description": "Relay server สำหรับกรณีที่ direct connection ไม่ได้ (double NAT)",
            "note": "Fallback เท่านั้น — ปกติ Tailscale ใช้ direct P2P",
        },
        "acl": {
            "name": "ACL (Access Control Lists)",
            "description": "กำหนดว่า device ไหนเข้าถึงอะไรได้ — JSON/HuJSON format",
            "example": "Developer group เข้า staging servers ได้, production ได้แค่ SRE",
        },
        "magicdns": {
            "name": "MagicDNS",
            "description": "DNS อัตโนมัติ — เรียก devices ด้วยชื่อ เช่น server1.tailnet-name.ts.net",
        },
    }

    def show_concepts(self):
        print("=== Tailscale Concepts ===\n")
        for key, concept in self.CONCEPTS.items():
            print(f"[{concept['name']}]")
            print(f"  {concept['description']}")
            print()

arch = TailscaleArchitecture()
arch.show_concepts()

Testing Strategy Overview

# testing_strategy.py — Testing strategy for Tailscale mesh
import json

class TestingStrategy:
    PYRAMID = {
        "unit": {
            "name": "Unit Tests (40%)",
            "scope": "ACL policy validation, DNS config, subnet routes, exit node config",
            "tools": "pytest, Tailscale policy test tool",
            "speed": "เร็วมาก (< 1 วินาที)",
            "examples": [
                "ACL syntax validation",
                "Tag ownership rules",
                "Auto-approve settings",
                "DNS configuration format",
            ],
        },
        "integration": {
            "name": "Integration Tests (30%)",
            "scope": "Device connectivity, ACL enforcement, subnet routing, MagicDNS resolution",
            "tools": "pytest + Tailscale CLI, Docker containers",
            "speed": "ปานกลาง (10-60 วินาที)",
            "examples": [
                "Device A can reach Device B on allowed port",
                "Device A blocked from Device C (ACL deny)",
                "Subnet route reaches internal network",
                "MagicDNS resolves correctly",
            ],
        },
        "e2e": {
            "name": "End-to-End Tests (20%)",
            "scope": "Full mesh connectivity, failover, performance, multi-user scenarios",
            "tools": "Custom scripts, iperf3, curl",
            "speed": "ช้า (1-10 นาที)",
            "examples": [
                "Full mesh: all N devices can reach each other",
                "DERP fallback when direct connection blocked",
                "Exit node routing works correctly",
                "Key rotation doesn't break connectivity",
            ],
        },
        "security": {
            "name": "Security Tests (10%)",
            "scope": "ACL bypass attempts, unauthorized access, encryption verification",
            "tools": "nmap, custom security scripts",
            "speed": "ช้า (5-30 นาที)",
            "examples": [
                "Unauthorized device cannot join tailnet",
                "ACL rules cannot be bypassed",
                "Traffic is encrypted (WireGuard verification)",
                "Expired keys are rejected",
            ],
        },
    }

    def show_pyramid(self):
        print("=== Test Pyramid ===\n")
        for key, level in self.PYRAMID.items():
            print(f"[{level['name']}]")
            print(f"  Scope: {level['scope']}")
            for ex in level["examples"][:3]:
                print(f"    • {ex}")
            print()

strategy = TestingStrategy()
strategy.show_pyramid()

Test Implementation

# tests.py — Tailscale test implementations
import json

class TailscaleTests:
    ACL_TESTS = """
# test_acl_policy.py — Unit tests for Tailscale ACL policy
import pytest
import json

# Sample ACL policy
ACL_POLICY = {
    "acls": [
        {"action": "accept", "src": ["group:dev"], "dst": ["tag:staging:*"]},
        {"action": "accept", "src": ["group:sre"], "dst": ["tag:production:*"]},
        {"action": "accept", "src": ["group:dev"], "dst": ["tag:production:80,443"]},
        {"action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"]},
    ],
    "groups": {
        "group:dev": ["user1@example.com", "user2@example.com"],
        "group:sre": ["sre1@example.com", "sre2@example.com"],
    },
    "tagOwners": {
        "tag:staging": ["group:dev"],
        "tag:production": ["group:sre"],
    },
}

class TestACLPolicy:
    def test_dev_can_access_staging(self):
        acls = ACL_POLICY["acls"]
        dev_to_staging = any(
            acl["action"] == "accept"
            and "group:dev" in acl["src"]
            and any("tag:staging" in dst for dst in acl["dst"])
            for acl in acls
        )
        assert dev_to_staging, "Dev group should access staging"
    
    def test_dev_cannot_full_access_production(self):
        acls = ACL_POLICY["acls"]
        dev_full_prod = any(
            acl["action"] == "accept"
            and "group:dev" in acl["src"]
            and "tag:production:*" in acl.get("dst", [])
            for acl in acls
        )
        assert not dev_full_prod, "Dev should NOT have full production access"
    
    def test_dev_limited_production_access(self):
        acls = ACL_POLICY["acls"]
        dev_limited = any(
            "group:dev" in acl["src"]
            and "tag:production:80,443" in acl["dst"]
            for acl in acls
        )
        assert dev_limited, "Dev should have port 80,443 access to production"
    
    def test_sre_full_production_access(self):
        acls = ACL_POLICY["acls"]
        sre_prod = any(
            "group:sre" in acl["src"]
            and "tag:production:*" in acl["dst"]
            for acl in acls
        )
        assert sre_prod, "SRE should have full production access"
    
    def test_tag_owners_valid(self):
        for tag, owners in ACL_POLICY["tagOwners"].items():
            for owner in owners:
                if owner.startswith("group:"):
                    assert owner in ACL_POLICY["groups"], f"Tag owner {owner} must exist in groups"
"""

    CONNECTIVITY_TESTS = """
# test_connectivity.py — Integration tests for mesh connectivity
import subprocess
import pytest
import time

class TestMeshConnectivity:
    @pytest.fixture
    def tailscale_status(self):
        result = subprocess.run(
            ["tailscale", "status", "--json"],
            capture_output=True, text=True
        )
        return json.loads(result.stdout)
    
    def test_tailscale_running(self):
        result = subprocess.run(["tailscale", "status"], capture_output=True, text=True)
        assert result.returncode == 0
    
    def test_device_online(self, tailscale_status):
        self_status = tailscale_status.get("Self", {})
        assert self_status.get("Online", False), "This device should be online"
    
    def test_peer_reachable(self):
        peers = ["server1", "server2", "server3"]
        for peer in peers:
            result = subprocess.run(
                ["tailscale", "ping", "-c", "3", "--timeout", "5s", peer],
                capture_output=True, text=True
            )
            assert result.returncode == 0, f"Peer {peer} should be reachable"
    
    def test_port_access_allowed(self):
        # Test allowed port (HTTP)
        result = subprocess.run(
            ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
             "--connect-timeout", "5", "http://staging-web:80"],
            capture_output=True, text=True
        )
        assert result.stdout.strip() in ["200", "301", "302"]
    
    def test_port_access_denied(self):
        # Test denied port (SSH to production for dev)
        result = subprocess.run(
            ["nc", "-z", "-w", "3", "production-db", "5432"],
            capture_output=True, text=True
        )
        assert result.returncode != 0, "DB port should be blocked by ACL"
    
    def test_magicdns_resolution(self):
        result = subprocess.run(
            ["nslookup", "server1.tailnet-name.ts.net"],
            capture_output=True, text=True
        )
        assert "100." in result.stdout, "MagicDNS should resolve to 100.x.y.z"
"""

    def show_acl_tests(self):
        print("=== ACL Tests ===")
        print(self.ACL_TESTS[:600])

    def show_connectivity(self):
        print(f"\n=== Connectivity Tests ===")
        print(self.CONNECTIVITY_TESTS[:600])

tests = TailscaleTests()
tests.show_acl_tests()
tests.show_connectivity()

Performance & Security Tests

# perf_security.py — Performance and security tests
import json
import random

class PerfSecurityTests:
    PERF_CODE = """
# test_performance.py — Network performance tests
import subprocess
import json
import time

class TestNetworkPerformance:
    def test_latency_acceptable(self):
        result = subprocess.run(
            ["tailscale", "ping", "-c", "20", "server1"],
            capture_output=True, text=True
        )
        lines = result.stdout.strip().split("\\n")
        latencies = []
        for line in lines:
            if "via" in line:
                ms = float(line.split("in")[1].strip().replace("ms", ""))
                latencies.append(ms)
        
        avg = sum(latencies) / len(latencies)
        p99 = sorted(latencies)[int(len(latencies) * 0.99)]
        
        assert avg < 50, f"Average latency {avg:.1f}ms should be < 50ms"
        assert p99 < 100, f"P99 latency {p99:.1f}ms should be < 100ms"
    
    def test_bandwidth(self):
        # iperf3 bandwidth test over Tailscale
        result = subprocess.run(
            ["iperf3", "-c", "server1", "-t", "10", "-J"],
            capture_output=True, text=True
        )
        data = json.loads(result.stdout)
        bps = data["end"]["sum_sent"]["bits_per_second"]
        mbps = bps / 1e6
        
        assert mbps > 100, f"Bandwidth {mbps:.0f} Mbps should be > 100 Mbps"
    
    def test_direct_vs_derp(self):
        result = subprocess.run(
            ["tailscale", "ping", "-c", "5", "server1"],
            capture_output=True, text=True
        )
        # Check if using direct connection (not DERP relay)
        assert "via DERP" not in result.stdout or "direct" in result.stdout
"""

    SECURITY_CODE = """
# test_security.py — Security tests
class TestSecurity:
    def test_unauthorized_device_blocked(self):
        # From an unauthorized device, try to reach tailnet
        result = subprocess.run(
            ["curl", "--connect-timeout", "3", "http://100.64.0.1:80"],
            capture_output=True, text=True
        )
        assert result.returncode != 0, "Unauthorized device should be blocked"
    
    def test_acl_bypass_attempt(self):
        # Try accessing denied resource through allowed resource
        result = subprocess.run(
            ["ssh", "-o", "ConnectTimeout=3", "staging-server",
             "curl", "-s", "http://production-db:5432"],
            capture_output=True, text=True
        )
        assert "refused" in result.stdout.lower() or result.returncode != 0
    
    def test_expired_key_rejected(self):
        # Check that key expiry is enforced
        status = json.loads(subprocess.run(
            ["tailscale", "status", "--json"],
            capture_output=True, text=True
        ).stdout)
        
        for peer_id, peer in status.get("Peer", {}).items():
            if peer.get("KeyExpiry"):
                # Verify expired keys are not active
                if peer["KeyExpiry"] < time.time():
                    assert not peer.get("Online", False)
"""

    def show_perf(self):
        print("=== Performance Tests ===")
        print(self.PERF_CODE[:500])

    def show_security(self):
        print(f"\n=== Security Tests ===")
        print(self.SECURITY_CODE[:500])

    def benchmark_results(self):
        print(f"\n=== Benchmark Results ===")
        print(f"  Direct connection:")
        print(f"    Latency: {random.uniform(1, 10):.1f}ms avg, {random.uniform(5, 20):.1f}ms P99")
        print(f"    Bandwidth: {random.uniform(200, 900):.0f} Mbps")
        print(f"  DERP relay:")
        print(f"    Latency: {random.uniform(30, 80):.1f}ms avg")
        print(f"    Bandwidth: {random.uniform(50, 200):.0f} Mbps")
        print(f"  Mesh (10 nodes): all pairs connected in {random.uniform(2, 8):.1f}s")

perf = PerfSecurityTests()
perf.show_perf()
perf.benchmark_results()

CI/CD Integration

# cicd.py — CI/CD pipeline for Tailscale testing
import json

class CICDPipeline:
    GITHUB_ACTIONS = """
# .github/workflows/tailscale-tests.yml
name: Tailscale Mesh Tests
on:
  push:
    paths: ['tailscale/**', 'acl/**']
  pull_request:
    paths: ['tailscale/**', 'acl/**']

jobs:
  acl-validation:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install pytest
      
      - name: Validate ACL policy
        run: |
          # Use Tailscale policy test tool
          curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale.gpg
          sudo apt-get update && sudo apt-get install -y tailscale
          
      - name: Run ACL unit tests
        run: pytest tests/unit/test_acl_policy.py -v

  connectivity-tests:
    needs: acl-validation
    runs-on: self-hosted  # Needs Tailscale access
    steps:
      - uses: actions/checkout@v4
      - uses: tailscale/github-action@v2
        with:
          oauth-client-id: }
          oauth-secret: }
          tags: tag:ci
      
      - name: Run connectivity tests
        run: pytest tests/integration/test_connectivity.py -v --timeout=120
      
      - name: Run performance tests
        run: pytest tests/performance/test_perf.py -v --timeout=300
"""

    def show_pipeline(self):
        print("=== CI/CD Pipeline ===")
        print(self.GITHUB_ACTIONS[:500])

    def test_matrix(self):
        print(f"\n=== Test Matrix ===")
        matrix = [
            {"test": "ACL validation", "env": "CI runner", "time": "< 5s", "status": "Pass"},
            {"test": "Connectivity", "env": "Self-hosted + Tailscale", "time": "~60s", "status": "Pass"},
            {"test": "Performance", "env": "Self-hosted + iperf3", "time": "~120s", "status": "Pass"},
            {"test": "Security", "env": "Isolated network", "time": "~180s", "status": "Pass"},
        ]
        for t in matrix:
            print(f"  [{t['status']:>4}] {t['test']:<20} {t['env']:<25} ({t['time']})")

cicd = CICDPipeline()
cicd.show_pipeline()
cicd.test_matrix()

FAQ - คำถามที่พบบ่อย

Q: Tailscale กับ WireGuard ต่างกันอย่างไร?

A: WireGuard: VPN protocol (low-level) — ต้อง configure เอง, manage keys เอง, ไม่มี NAT traversal อัตโนมัติ Tailscale: mesh VPN ที่ใช้ WireGuard — auto key exchange, NAT traversal, ACLs, MagicDNS, SSO integration Tailscale = WireGuard + management layer + UX ที่ดีมาก ทางเลือก: Headscale (self-hosted Tailscale coordination server — open source)

Q: Test Tailscale mesh ยากไหม?

A: ACL tests: ง่าย — validate JSON/HuJSON policy locally Connectivity tests: ปานกลาง — ต้องมี devices ใน tailnet จริง Performance: ต้อง iperf3 + real network conditions ใช้ Tailscale GitHub Action: ง่ายสำหรับ CI — auto-join tailnet แล้ว test Docker containers + Tailscale: จำลอง mesh ได้ใน CI

Q: ACL policy ควร test อะไรบ้าง?

A: Must test: Syntax valid, groups มี members, tag owners ถูกต้อง ทุก group เข้าถึงเฉพาะที่ควรเข้าถึง Deny by default: ถ้าไม่มี rule → ต้อง blocked Production access: จำกัดเฉพาะ authorized groups Sensitive ports: DB, SSH ต้องจำกัดให้เฉพาะ SRE/DBA

Q: Tailscale เหมาะกับ production ไหม?

A: เหมาะมาก — หลายบริษัทใช้ใน production: ข้อดี: ง่ายมาก, WireGuard encryption, SSO, ACLs, audit logs ข้อดี: NAT traversal อัตโนมัติ — ไม่ต้อง open ports ข้อจำกัด: coordination server เป็น SaaS (ถ้าต้องการ self-host ใช้ Headscale) Pricing: Free 100 devices, Personal $5/user, Business $18/user