SiamCafe.net Blog
Technology

Tailscale Mesh Testing Strategy QA

tailscale mesh testing strategy qa
Tailscale Mesh Testing Strategy QA | SiamCafe Blog
2025-07-09· อ. บอม — SiamCafe.net· 1,581 คำ

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

📖 บทความที่เกี่ยวข้อง

Tailscale Mesh Business Continuityอ่านบทความ → Tailscale Mesh Architecture Design Patternอ่านบทความ → Tailscale Mesh Home Lab Setupอ่านบทความ → Tailscale Mesh Schema Evolutionอ่านบทความ → PostgreSQL Full Text Search Testing Strategy QAอ่านบทความ →

📚 ดูบทความทั้งหมด →