Project 9: ZTNA App Tunnel (No More VPNs!)
Project 9: ZTNA App Tunnel (No More VPNs!)
Core Zero Trust Principle: “Connect users to applications, never to networks.” This project teaches you to build the modern replacement for VPNs - a client agent that selectively tunnels traffic to specific applications while leaving all other internet traffic untouched.
Project Overview
| Attribute | Value |
|---|---|
| Difficulty | Level 4: Expert |
| Time Estimate | 2 Weeks |
| Main Language | Rust or Go |
| Alternative Languages | C |
| Knowledge Area | L4/L7 Tunneling |
| Key Technologies | SOCKS5, HTTP CONNECT, mTLS, HTTP/2 |
| Main Book | “TCP/IP Illustrated, Volume 1” by W. Richard Stevens |
| Prerequisites | Project 1 (Identity-Aware Proxy), Project 4 (mTLS Mesh) |
What You’re Building: A client agent that intercepts traffic only for specific internal URLs (e.g., internal.corp.com) and tunnels it over a secure mTLS connection to your Project 1 Proxy. It DOES NOT touch the user’s other internet traffic.
Why It Matters: Traditional VPNs are fundamentally incompatible with Zero Trust because they place the user’s entire device “inside” the network. ZTNA App Tunnels flip this model - they bring the application to the user, not the user to the network. This eliminates the primary vector for lateral movement after device compromise.
Learning Objectives
By completing this project, you will be able to:
- Implement a SOCKS5 proxy from scratch - Parse RFC 1928 messages, handle authentication, and support TCP connections
- Design selective traffic routing - Build a domain-based routing engine that splits tunnel traffic from direct traffic
- Prevent DNS leaks - Understand and implement DNS interception to protect user privacy
- Establish mTLS upstream connections - Reuse concepts from Project 4 to secure the tunnel
- Implement HTTP/2 stream multiplexing - Optimize tunnel performance for multiple concurrent connections
- Inject identity into tunneled requests - Carry user and device identity through the tunnel to the gateway
- Articulate ZTNA vs VPN tradeoffs - Explain why application-level access is superior to network-level access
The Core Question You’re Answering
“How can I replace traditional VPNs with application-specific, identity-aware tunnels that provide better security and user experience?”
Traditional VPNs operate on a fundamentally flawed assumption: that network location equals trust. When a user connects via VPN, their device is virtually “inside” the corporate network with full Layer 3 connectivity to potentially thousands of hosts. This is the digital equivalent of giving someone a master key to your building when they only need access to one office.
ZTNA flips this model entirely. Instead of bringing the user to the network, ZTNA brings the application to the user. The user’s device never gains network-level access - it only receives access to specific, authorized applications through a secure tunnel. Even if the user’s device is compromised, the attacker can only reach the 2-3 applications that user was authorized for, not scan the entire 10.0.0.0/8 network for vulnerable hosts.
The security implications are profound:
- VPN attack surface: Millions of IP:port combinations accessible after connection
- ZTNA attack surface: Only the specific application endpoints the user needs
- Lateral movement potential: VPN enables it; ZTNA prevents it by design
Concepts You Must Understand First
Before implementing a ZTNA App Tunnel, ensure you have a solid grasp of these foundational concepts:
1. SOCKS5 Proxy Protocol (RFC 1928)
SOCKS5 is a Layer 4 (Transport) proxy protocol that can tunnel any TCP or UDP traffic. Unlike HTTP proxies which only understand HTTP, SOCKS5 is protocol-agnostic - it simply forwards raw bytes between client and destination.
Key mechanisms:
- Authentication negotiation (version, methods)
- CONNECT command for TCP connections
- UDP ASSOCIATE for UDP traffic
- Address types: IPv4, IPv6, and domain names
Why it matters: SOCKS5 lets your ZTNA client tunnel SSH, database connections, RDP, and any custom protocol - not just web traffic.
2. Split Tunneling and Domain-Based Routing
Split tunneling means selectively routing some traffic through the tunnel while letting other traffic go directly to the internet. For ZTNA, this is the default (not an option).
Key mechanisms:
- Domain whitelist matching (exact, wildcard, suffix)
- IP range matching (CIDR notation)
- PAC (Proxy Auto-Configuration) files
- Routing table manipulation
Why it matters: You only tunnel what needs protection. Internet traffic stays fast, tunnel bandwidth stays low, and user privacy is preserved.
3. mTLS for Tunnel Security
Mutual TLS (mTLS) ensures both the client and gateway cryptographically prove their identity. The tunnel cannot be intercepted, and both endpoints are authenticated.
Key mechanisms:
- Client certificate containing user/device identity
- Server certificate containing gateway identity
- Certificate chain validation against trusted CA
- SPIFFE IDs for workload identity
Why it matters: Without mTLS, an attacker could impersonate either the client or the gateway. With mTLS, the tunnel is cryptographically authenticated in both directions.
4. DNS Leak Prevention
A DNS leak occurs when DNS queries for internal domains (e.g., jira.internal) are sent to public DNS servers instead of being routed through the secure tunnel.
Key mechanisms:
- SOCKS5h (DNS resolved by proxy)
- Split-horizon DNS
- OS-level DNS interception (Network Extension, WFP, eBPF)
- DNS-over-HTTPS/TLS through tunnel
Why it matters: DNS leaks expose your internal infrastructure to anyone watching the network. An attacker learns which internal services exist before even attempting to breach them.
5. HTTP/2 Multiplexing
HTTP/2 allows multiple streams (requests/responses) to share a single TCP connection. For tunneling, this means one mTLS handshake can serve dozens of concurrent application connections.
Key mechanisms:
- Stream identifiers for parallel requests
- Flow control per stream
- Header compression (HPACK)
- Server push (less relevant for tunneling)
Why it matters: Without multiplexing, each new application connection requires a new mTLS handshake (100-300ms). With multiplexing, the overhead is near zero after the first connection.
6. Identity Propagation Through Tunnels
The tunnel must carry user and device identity from the client to the gateway, so the gateway can inject identity headers into requests to internal services.
Key mechanisms:
- Identity in TLS client certificate (CN, SPIFFE ID)
- Custom headers in tunnel protocol (X-ZT-Identity)
- Identity extraction at gateway
- Trust relationship between gateway and internal services
Why it matters: This enables “login-free” access to internal applications. The user’s identity is already verified by the tunnel - no need for additional authentication at each app.
Questions to Guide Your Design
Before writing code, work through these architectural questions:
Tunnel Architecture
-
Client-side interception: How will you intercept traffic on the user’s machine? System proxy settings, PAC file, transparent proxy (iptables/pf), or TUN/TAP interface?
-
Protocol choice: Will you use SOCKS5, HTTP CONNECT, or both? What are the tradeoffs for your use case?
-
Gateway integration: How does your tunnel client communicate with the ZTNA gateway (Project 1)? HTTP/2 CONNECT? gRPC? Custom protocol?
-
Connection lifecycle: How do you handle gateway disconnection? Do you queue requests, fail fast, or retry transparently?
Routing Decisions
-
Domain matching: How do you efficiently match domains against patterns like
*.internal,*.corp.com, and10.0.0.0/8? -
Decision caching: Should you cache routing decisions? What’s the invalidation strategy?
-
Configuration updates: Can you update the domain whitelist without restarting the client?
Security Considerations
-
DNS handling: How do you ensure DNS for internal domains goes through the tunnel? What about apps that don’t use SOCKS5?
-
Identity sourcing: Where does the client get the user and device identity? Certificate? Config file? OS-level attestation?
-
Certificate lifecycle: How do you handle certificate expiration and rotation without dropping active connections?
Thinking Exercise
Before implementing, trace a request through your ZTNA tunnel on paper. This exercise will reveal gaps in your understanding.
Exercise: Trace a Request to jira.internal
Grab a piece of paper and draw the flow for this scenario:
Setup:
- User’s browser configured to use SOCKS5 proxy at 127.0.0.1:1080
- ZTNA client running, connected to gateway at gateway.corp.com:8443
- Domain whitelist includes
*.internal - User types
https://jira.internal/dashboardin browser
Trace each step:
- What happens in the browser before any network traffic?
- What SOCKS5 messages are exchanged with your client?
- How does your client decide to tunnel vs bypass?
- What does the tunnel traffic look like on the wire?
- How does the gateway know the user’s identity?
- What headers does jira.internal receive?
- How does the response flow back?
Consider edge cases:
- What if the gateway is unreachable?
- What if the certificate is expired?
- What if DNS for
jira.internalleaks to public DNS before reaching your SOCKS5 server? - What if 10 browser tabs open simultaneously?
After completing this exercise, you should be able to draw a complete sequence diagram with all the protocol details.
Hints in Layers
Use these hints progressively if you get stuck. Try to solve each challenge yourself before revealing the next hint.
Hint 1: SOCKS5 Implementation Starting Point
Start by implementing a “pass-through” SOCKS5 proxy that connects directly to any target. This lets you test the SOCKS5 protocol handling before adding routing logic.
Click to reveal structure hint
The SOCKS5 handshake has two phases:
- Authentication negotiation (client sends methods, server picks one)
- Connection request (client sends target, server connects and responds)
After the handshake succeeds, you just forward bytes bidirectionally - no more protocol parsing needed.
Hint 2: Domain Matching Efficiency
For efficient domain matching, don’t iterate through patterns for every request. Pre-process your whitelist into data structures optimized for lookup.
Click to reveal algorithm hint
- Use a hash set for exact matches (O(1) lookup)
- For wildcard patterns like
*.internal, extract the suffix (.internal) and check if the domain ends with it - For IP ranges, parse CIDR into network/mask and check containment
- Consider a trie structure for very large whitelists
Hint 3: HTTP/2 Connection Reuse
The key to efficient multiplexing is reusing the same http.Client (Go) or connection pool for all tunnel streams.
Click to reveal implementation hint
In Go:
transport := &http2.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{Transport: transport}
// Reuse this client for ALL tunnel connections
Each http.NewRequest("CONNECT", ...) becomes a new stream on the same HTTP/2 connection.
Hint 4: DNS Leak Prevention Strategy
The easiest DNS leak prevention is using socks5h:// (note the ‘h’) which tells the client to send the domain name to the SOCKS5 proxy for resolution.
Click to reveal complete strategy
- Configure clients to use
socks5h://127.0.0.1:1080(DNS through proxy) - In your SOCKS5 server, receive the domain name (ATYP 0x03), not an IP
- For internal domains, resolve through the tunnel
- For external domains, you can either resolve locally or let the gateway resolve
For apps that don’t support SOCKS5h, you’ll need OS-level DNS interception:
- macOS: Network Extension framework
- Linux: eBPF or iptables REDIRECT to local DNS server
- Windows: WFP (Windows Filtering Platform)
Hint 5: Graceful Gateway Reconnection
Don’t fail requests immediately when the gateway disconnects. Give the reconnection logic a chance to succeed.
Click to reveal reconnection pattern
- Maintain a connection state: CONNECTED, CONNECTING, DISCONNECTED
- When a request comes in and state is CONNECTED: send immediately
- When state is CONNECTING: queue the request with a timeout
- When state is DISCONNECTED: start reconnection, queue the request
- Use exponential backoff for reconnection attempts
- After max retries, fail queued requests with clear error message
- Keep the SOCKS5 server running - it can still handle bypassed traffic
Deep Theoretical Foundation
VPN vs ZTNA: The Fundamental Paradigm Shift
Understanding why ZTNA exists requires understanding why VPNs are fundamentally broken for modern security.
+--------------------------------------------------------------------------+
| Traditional VPN Model |
+--------------------------------------------------------------------------+
CORPORATE NETWORK
+---------------------------+
| |
[Remote User] | [ Jira ] [ GitLab ] |
| | | | |
| | [ Database Server ] |
| | | | |
[ VPN Client ] | [ All Internal Hosts ] |
| | | |
+----------+----[ Firewall ]-----+-----+
|
+-----------+-----------+
| |
[ Internet ] [ VPN Tunnel ]
| |
| |
[ User's Laptop ] |
ALL traffic now |
goes through VPN <--------+
THE PROBLEM:
- User's laptop is now INSIDE the corporate network
- Malware on the laptop can scan 10.0.0.0/8
- Compromised user has NETWORK access, not just APP access
- Even traffic to google.com goes through the VPN (latency!)
- VPN concentrator becomes a single point of failure
+--------------------------------------------------------------------------+
+--------------------------------------------------------------------------+
| Zero Trust Network Access (ZTNA) Model |
+--------------------------------------------------------------------------+
[ User's Laptop ]
|
+-----------+-----------+
| |
[ ZTNA Client Agent ] [ Normal Internet ]
| |
| +---> google.com (direct)
| +---> youtube.com (direct)
| +---> github.com (direct)
|
| (ONLY internal app traffic)
|
[ mTLS Tunnel ]
|
v
+-----+-----+
| |
| ZTNA | The ZTNA Gateway is typically
| Gateway | your Identity-Aware Proxy (Project 1)
| (PEP) |
| |
+-----+-----+
|
| (Authenticated, authorized request)
|
v
[ jira.internal ] <-- User can ONLY reach specific apps
NOT the network!
THE ADVANTAGE:
- User's laptop remains OUTSIDE the corporate network
- Only APPLICATION traffic is tunneled, not network traffic
- No ability to scan internal IP ranges
- Compromise is limited to specific authorized applications
- Internet traffic stays fast (no VPN hop)
- No single point of failure (distributed gateways)
+--------------------------------------------------------------------------+
Key Insight: In VPN, trust is established by network location (you’re “inside”). In ZTNA, trust is established by identity (who you are + what app you’re accessing + what device you’re using).
The Attack Surface Comparison
+--------------------------------------------------------------------------+
| Attack Surface: VPN vs ZTNA |
+--------------------------------------------------------------------------+
VPN ATTACK SURFACE (After Connection):
+------------------------------------------------------------------+
| |
| User gets: Full Layer 3 connectivity to 10.0.0.0/8 |
| |
| [ Laptop ] -----> [ Any host in 10.0.0.0/8 ] |
| |
| Attacker can: |
| - Port scan 10.0.0.1 to 10.255.255.254 |
| - Connect to any open port on any host |
| - Exploit unpatched internal services |
| - Pivot through compromised hosts |
| - Access databases directly (bypass app) |
| - Exfiltrate data to any internal host |
| |
| Attack surface: MILLIONS of IP:PORT combinations |
| |
+------------------------------------------------------------------+
ZTNA ATTACK SURFACE (After Connection):
+------------------------------------------------------------------+
| |
| User gets: Access to authorized applications ONLY |
| |
| [ Laptop ] --X--> [ Cannot reach 10.0.0.0/8 ] |
| |
| [ Laptop ] -----> [ jira.internal:443 ] (if authorized) |
| -----> [ gitlab.internal:443 ] (if authorized) |
| --X--> [ database:5432 ] (NOT authorized) |
| |
| Attacker can: |
| - Access only the specific apps the user is authorized for |
| - That's it. |
| |
| Attack surface: 2-3 specific application endpoints |
| |
+------------------------------------------------------------------+
REDUCTION FACTOR: 99.999%+ reduction in attack surface
Split Tunneling: The Default for Zero Trust
Split tunneling means some traffic goes through the tunnel while other traffic goes directly to the internet. This is the default for ZTNA, while traditional VPNs often use full tunneling.
+--------------------------------------------------------------------------+
| Full Tunnel vs Split Tunnel |
+--------------------------------------------------------------------------+
FULL TUNNEL (Traditional VPN Default):
----------------------------------------
[ User's Laptop ]
|
| ALL TRAFFIC
|
v
[ VPN Tunnel ] ---------> [ VPN Server ] ---------> [ Internet ]
|
+---------> [ Internal Apps ]
- google.com goes through VPN (latency++)
- netflix.com goes through VPN (bandwidth!!)
- Corporate traffic goes through VPN (intended)
Problems:
- VPN bandwidth saturated by streaming
- Increased latency for all traffic
- User experience degrades
- VPN server sees all user traffic (privacy)
SPLIT TUNNEL (ZTNA Default):
-----------------------------
[ User's Laptop ]
|
+----- [ ZTNA Agent ] -----> [ ZTNA Gateway ] --> [ Internal Apps ]
| |
| Domain Match?
| |
| *.internal? --> TUNNEL
| *.corp.com? --> TUNNEL
| else --> BYPASS
|
+----- [ Direct Connection ] -----> [ Internet ]
- google.com goes DIRECT (fast!)
- netflix.com goes DIRECT (no bandwidth on tunnel)
- jira.internal goes through TUNNEL (intended)
Advantages:
- Optimal user experience
- Minimal tunnel bandwidth
- No visibility into personal traffic
- Scales to millions of users
+--------------------------------------------------------------------------+
| Security Implications |
+--------------------------------------------------------------------------+
Q: "Isn't split tunneling less secure? Traffic isn't protected."
A: This reveals a fundamental misunderstanding.
The PURPOSE of ZTNA is to protect INTERNAL applications from
external threats, NOT to protect users from the internet.
If you need to protect users from malicious sites, use:
- DNS filtering (Pi-hole, Cloudflare Gateway)
- Secure Web Gateway (SWG)
- Browser isolation
ZTNA protects the castle. Other tools protect the knight.
+--------------------------------------------------------------------------+
The SOCKS5 Protocol
SOCKS5 (RFC 1928) is a Layer 4 (Transport Layer) proxy protocol. Unlike HTTP proxies, it can handle any TCP/UDP traffic, not just HTTP.
+--------------------------------------------------------------------------+
| SOCKS5 Protocol Overview (RFC 1928) |
+--------------------------------------------------------------------------+
WHY SOCKS5 FOR ZTNA?
Layer 7 Proxy (HTTP):
- Understands HTTP methods (GET, POST, etc.)
- Can only proxy HTTP/HTTPS traffic
- Cannot proxy SSH, database connections, or custom protocols
Layer 4 Proxy (SOCKS5):
- Works at the TCP/UDP level
- Can proxy ANY protocol: HTTP, SSH, PostgreSQL, RDP, etc.
- Perfect for "Application Tunnel" use case
+--------------------------------------------------------------------------+
| SOCKS5 Connection Flow |
+--------------------------------------------------------------------------+
Client SOCKS5 Proxy Target
| | |
| 1. Authentication Negotiation | |
| --------------------------------> | |
| VER | NMETHODS | METHODS | |
| 0x05| 0x01 | 0x00 | |
| (v5)| (1 method)| (no auth) | |
| | |
| <-------------------------------- | |
| VER | METHOD | |
| 0x05| 0x00 | |
| (v5)| (no auth selected) | |
| | |
| 2. Connection Request | |
| --------------------------------> | |
| VER|CMD|RSV|ATYP| DST.ADDR | DST.PORT |
| 0x05|0x01|0x00|0x03| [domain] | [port] |
| (v5)|(connect) | | |
| | |
| | 3. Proxy connects to target |
| | ----------------------------->|
| | |
| <-------------------------------- | |
| VER|REP|RSV|ATYP| BND.ADDR | BND.PORT |
| 0x05|0x00|0x00|0x01| [ip] | [port] |
| (v5)|(success) | | |
| | |
| 4. Data Transfer (bidirectional) | |
| <================================>|<============================>|
| Raw TCP bytes, any protocol | Raw TCP bytes |
| | |
+--------------------------------------------------------------------------+
| SOCKS5 Packet Formats |
+--------------------------------------------------------------------------+
AUTHENTICATION NEGOTIATION REQUEST:
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1-255 | bytes
+----+----------+----------+
VER: 0x05 (SOCKS version 5)
NMETHODS: Number of authentication methods supported
METHODS: List of method identifiers
0x00: No authentication required
0x01: GSSAPI
0x02: Username/Password (RFC 1929)
0x03-0x7F: IANA assigned
0x80-0xFE: Reserved for private use
0xFF: No acceptable methods
AUTHENTICATION NEGOTIATION RESPONSE:
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 | bytes
+----+--------+
Server picks one method from client's list
0xFF = no acceptable method (close connection)
CONNECTION REQUEST:
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 | bytes
+----+-----+-------+------+----------+----------+
CMD:
0x01: CONNECT (establish TCP connection)
0x02: BIND (for protocols that need inbound connections)
0x03: UDP ASSOCIATE (for UDP traffic)
ATYP (Address Type):
0x01: IPv4 (4 bytes)
0x03: Domain name (1 byte length + name)
0x04: IPv6 (16 bytes)
DST.ADDR:
For 0x01: 4 bytes IPv4 address
For 0x03: 1 byte length + domain string
For 0x04: 16 bytes IPv6 address
DST.PORT: 2 bytes, big-endian
CONNECTION RESPONSE:
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 | bytes
+----+-----+-------+------+----------+----------+
REP (Reply):
0x00: Succeeded
0x01: General SOCKS server failure
0x02: Connection not allowed by ruleset
0x03: Network unreachable
0x04: Host unreachable
0x05: Connection refused
0x06: TTL expired
0x07: Command not supported
0x08: Address type not supported
+--------------------------------------------------------------------------+
HTTP CONNECT Method
HTTP CONNECT is the Layer 7 alternative to SOCKS5 for TCP tunneling. It’s how browsers tunnel HTTPS through HTTP proxies.
+--------------------------------------------------------------------------+
| HTTP CONNECT Method |
+--------------------------------------------------------------------------+
When a browser wants to access https://example.com through a proxy,
it can't just send the GET request - the proxy can't decrypt HTTPS.
Instead, the browser asks the proxy to establish a raw TCP tunnel:
Client HTTP Proxy Target
| | |
| CONNECT example.com:443 HTTP/1.1 | |
| Host: example.com:443 | |
| ----------------------------------> | |
| | |
| | (proxy connects to target) |
| | ---------------------------> |
| | |
| <---------------------------------- | <--------------------------- |
| HTTP/1.1 200 Connection Established | |
| | |
| (TLS handshake begins) | |
| <================================================================> |
| (now raw TLS bytes flow through) | |
| | |
| GET / HTTP/1.1 | |
| (encrypted, proxy can't read) | |
| <================================================================> |
| | |
+--------------------------------------------------------------------------+
SOCKS5 vs HTTP CONNECT:
-----------------------
| Feature | SOCKS5 | HTTP CONNECT |
|--------------------|---------------------|------------------------|
| Layer | 4 (Transport) | 7 (Application) |
| Protocol Support | Any TCP/UDP | Only TCP |
| UDP Support | Yes (UDP ASSOCIATE) | No |
| Complexity | Higher | Lower |
| Browser Support | Via extension/PAC | Native |
| Non-HTTP protocols | SSH, DB, RDP, etc. | Only via TCP tunnel |
| Authentication | Multiple methods | HTTP auth headers |
For ZTNA, SOCKS5 is preferred because it supports:
- SSH connections to internal servers
- Database connections (PostgreSQL, MySQL)
- RDP for remote desktop
- Any custom internal protocol
+--------------------------------------------------------------------------+
Layer 4 vs Layer 7 Proxies: What Each Can See
Understanding the difference is crucial for designing the right proxy for your use case.
+--------------------------------------------------------------------------+
| Layer 4 vs Layer 7 Visibility |
+--------------------------------------------------------------------------+
THE OSI MODEL (Simplified):
Layer 7: Application [ HTTP, SSH, DNS, PostgreSQL ]
Layer 6: Presentation [ TLS/SSL ]
Layer 5: Session [ Session management ]
Layer 4: Transport [ TCP, UDP ]
Layer 3: Network [ IP ]
Layer 2: Data Link [ Ethernet, WiFi ]
Layer 1: Physical [ Cables, Radio ]
LAYER 4 PROXY (SOCKS5, TCP Load Balancer):
------------------------------------------
[ Client ] --> [ L4 Proxy ] --> [ Server ]
What the proxy sees:
+------------------------------------------+
| Source IP: 192.168.1.100 |
| Destination IP: 10.0.1.50 (or domain) |
| Source Port: 52431 |
| Destination Port: 443 |
| Protocol: TCP |
| Raw bytes: 0x16 0x03 0x01 0x02 0x00 ... | <- TLS handshake (opaque)
+------------------------------------------+
What the proxy CANNOT see:
- HTTP method (GET, POST, etc.)
- HTTP headers (Host, Cookie, Authorization)
- HTTP body (JSON, form data)
- URL path (/api/users)
- TLS certificate details (SNI only)
Perfect for: Tunneling any TCP protocol
LAYER 7 PROXY (HTTP Proxy, Reverse Proxy):
------------------------------------------
[ Client ] --> [ L7 Proxy ] --> [ Server ]
What the proxy sees (for HTTP):
+------------------------------------------+
| Method: GET |
| Path: /api/users/123 |
| Host: api.example.com |
| Headers: |
| Authorization: Bearer eyJhbG... |
| Content-Type: application/json |
| Cookie: session=abc123 |
| Body: {"name": "douglas"} |
+------------------------------------------+
What the proxy can do:
- Inspect and modify headers
- Inject identity headers (X-ZT-Identity)
- Route based on URL path
- Cache responses
- Transform requests
Cannot proxy: SSH, PostgreSQL, RDP, or any non-HTTP protocol
FOR ZTNA APP TUNNEL:
-------------------
We use BOTH layers:
1. SOCKS5 (L4) on the client side:
- Accepts connections for ANY protocol
- Routes based on domain/IP only
2. The tunnel itself:
- Carries raw TCP bytes to the gateway
- Uses HTTP/2 or QUIC for multiplexing
3. HTTP Reverse Proxy (L7) on the gateway:
- Unwraps the tunnel
- Inspects HTTP traffic for identity injection
- Forwards to internal services
+--------------------------------------------------------------------------+
mTLS for Tunnel Security
The tunnel between the client agent and the ZTNA gateway MUST use mTLS. This ensures both ends cryptographically prove their identity.
+--------------------------------------------------------------------------+
| mTLS in ZTNA Tunnel |
+--------------------------------------------------------------------------+
WITHOUT mTLS:
-------------
[ Attacker ] -----> [ ZTNA Gateway ]
|
+-- "I'm the legitimate client agent"
+-- (No proof required)
+-- (Attacker can spoof)
WITH mTLS:
----------
[ Client Agent ] [ ZTNA Gateway ]
| |
| ClientHello + Client Certificate |
| ---------------------------------------->|
| |
| ServerHello + Server Certificate
| <----------------------------------------|
| |
| Certificate Verify (signed with |
| client private key) |
| ---------------------------------------->|
| |
| Gateway verifies: |
| - Client cert signed by trusted CA
| - SPIFFE ID matches allowed clients
| - Device ID is registered
| |
| If verification fails: CLOSE CONNECTION
| |
| Encrypted Tunnel |
| <=======================================>|
| |
WHY mTLS IS ESSENTIAL:
----------------------
1. CLIENT AUTHENTICATION:
- Gateway knows WHICH client agent is connecting
- Certificate contains user identity (e.g., douglas@corp.com)
- Certificate contains device identity (e.g., macbook-42)
2. SERVER AUTHENTICATION:
- Client knows it's connecting to the REAL gateway
- Prevents man-in-the-middle attacks
- Certificate pinning prevents CA compromise attacks
3. ENCRYPTION:
- Traffic is encrypted against Wi-Fi sniffing
- Even on hostile networks (hotel, airport)
4. REPLAY PREVENTION:
- Each TLS session has unique keys
- Captured traffic cannot be replayed
CONNECTION TO PROJECT 4:
------------------------
The mTLS concepts you learned in Project 4 apply directly here:
- Use short-lived certificates (1-hour)
- Rotate certificates automatically
- Include SPIFFE ID in certificate SAN
- Trust only your organizational CA
+--------------------------------------------------------------------------+
DNS Leaks: The Critical Security Vulnerability
DNS leaks are one of the most common and dangerous vulnerabilities in split-tunnel implementations. If internal DNS queries go to external DNS servers, an attacker can learn what internal services exist.
+--------------------------------------------------------------------------+
| DNS Leaks Explained |
+--------------------------------------------------------------------------+
WHAT IS A DNS LEAK?
-------------------
User wants to access: jira.internal
CORRECT BEHAVIOR (No Leak):
Browser ZTNA Agent Internal DNS
| | |
| "Resolve jira.internal" | |
| ----------------------->| |
| | "Resolve jira.internal" |
| | ----------------------->|
| | |
| | <-----------------------|
| | "10.0.1.50" |
| <-----------------------| |
| "10.0.1.50" | |
| | |
The DNS query went through the secure tunnel.
External observers see NOTHING.
DNS LEAK (Vulnerable):
Browser Public DNS (8.8.8.8)
| |
| "Resolve jira.internal" |
| -------------------------------------------->|
| |
| <--------------------------------------------|
| "NXDOMAIN" (domain doesn't exist) |
| |
But the DNS server now knows:
- This user's IP address
- They tried to access "jira.internal"
- This company uses Jira internally
- The naming convention is *.internal
This information can be used for:
- Targeted phishing attacks
- Reconnaissance for network attacks
- Building a map of internal services
+--------------------------------------------------------------------------+
| Types of DNS Leaks |
+--------------------------------------------------------------------------+
1. WEBRTC LEAK:
Browser uses WebRTC to get public IP
Bypasses proxy settings entirely
2. IPv6 LEAK:
Tunnel handles IPv4 only
IPv6 DNS queries go direct
3. SYSTEM DNS LEAK:
OS sends DNS query before checking proxy
Common on macOS and Windows
4. TIMING LEAK:
Parallel DNS queries to multiple servers
Internal query times differ from external
+--------------------------------------------------------------------------+
| Solutions for DNS Leak Prevention |
+--------------------------------------------------------------------------+
SOLUTION 1: DNS-over-HTTPS (DoH) / DNS-over-TLS (DoT)
-----------------------------------------------------
All DNS queries encrypted through the tunnel:
Browser -> ZTNA Agent -> mTLS Tunnel -> Gateway -> Internal DNS
\ /
\---- DoH to gateway.corp.com/dns-query -----/
SOLUTION 2: Split-Horizon DNS
-----------------------------
Different DNS responses based on source:
External DNS: jira.internal -> NXDOMAIN
Internal DNS: jira.internal -> 10.0.1.50
Configure ZTNA client to:
1. Check if domain matches internal patterns (*.internal, *.corp.com)
2. If yes, route DNS through tunnel
3. If no, use local DNS server
SOLUTION 3: DNS Interception
----------------------------
The ZTNA agent intercepts DNS at the OS level:
1. Hook into OS DNS resolution
2. Check if domain matches whitelist
3. If internal: forward through tunnel
4. If external: allow normal resolution
Implementation:
- macOS: Network Extension framework
- Windows: WFP (Windows Filtering Platform)
- Linux: eBPF or iptables REDIRECT
SOLUTION 4: PAC File with DNS Resolve
-------------------------------------
Proxy Auto-Config (PAC) files can control routing:
function FindProxyForURL(url, host) {
// Check if internal domain
if (shExpMatch(host, "*.internal") ||
shExpMatch(host, "*.corp.com")) {
// Also resolve DNS through proxy
return "SOCKS5 127.0.0.1:1080";
}
return "DIRECT";
}
NOTE: PAC files have limitations - they work at connection time,
not DNS resolution time. Use with DNS interception.
+--------------------------------------------------------------------------+
| Testing for DNS Leaks |
+--------------------------------------------------------------------------+
TEST 1: Wireshark Capture
$ sudo tcpdump -i en0 port 53
# Then access jira.internal
# If you see the query on port 53, you have a leak
TEST 2: Check DNS Query Destination
$ dig jira.internal
# Check if it goes to internal or external DNS
TEST 3: Online Tools (for external leaks)
https://www.dnsleaktest.com/
https://ipleak.net/
+--------------------------------------------------------------------------+
Traffic Interception Techniques
How does the ZTNA client actually intercept traffic? There are several approaches with different tradeoffs.
+--------------------------------------------------------------------------+
| Traffic Interception Methods |
+--------------------------------------------------------------------------+
METHOD 1: System Proxy Settings
-------------------------------
Configure OS to use SOCKS5 proxy at 127.0.0.1:1080
Advantages:
- No special privileges needed
- Works with all apps that respect system proxy
- Easy to implement
Disadvantages:
- Some apps ignore system proxy (curl -x, wget, etc.)
- User can bypass by disabling proxy
- DNS might still leak
Implementation:
- macOS: networksetup -setsocksproxy Wi-Fi 127.0.0.1 1080
- Windows: Registry or netsh
- Linux: environment variables + /etc/environment
METHOD 2: Proxy Auto-Configuration (PAC) File
----------------------------------------------
JavaScript file that determines routing per-request:
function FindProxyForURL(url, host) {
// Internal domains go through tunnel
if (shExpMatch(host, "*.internal")) {
return "SOCKS5 127.0.0.1:1080";
}
if (shExpMatch(host, "*.corp.com")) {
return "SOCKS5 127.0.0.1:1080";
}
// Everything else goes direct
return "DIRECT";
}
Advantages:
- Fine-grained routing rules
- No need to tunnel all traffic
- Widely supported
Disadvantages:
- Only works for browser traffic
- PAC file must be hosted somewhere
- No DNS control
METHOD 3: Transparent Proxy (iptables/pf)
-----------------------------------------
Redirect traffic at the network layer without app awareness:
Linux (iptables):
$ iptables -t nat -A OUTPUT -p tcp --dport 443 \
-m owner --uid-owner $UID \
-j REDIRECT --to-ports 1080
macOS (pf):
$ echo "rdr pass on lo0 proto tcp to any port 443 -> 127.0.0.1 port 1080" \
>> /etc/pf.anchors/ztna
Advantages:
- Works with ALL applications
- No app configuration needed
- Can intercept DNS
Disadvantages:
- Requires root/admin
- OS-specific implementation
- Can be complex to configure correctly
METHOD 4: TUN/TAP Virtual Interface
------------------------------------
Create a virtual network interface and route traffic to it:
[ Application ]
|
[ Routing Table ]
|
+-- 10.0.0.0/8 --> tun0 (ZTNA agent)
|
+-- 0.0.0.0/0 --> en0 (physical interface)
[ tun0 ] -----> [ ZTNA Agent ] -----> [ mTLS Tunnel ]
Advantages:
- Full control over all traffic
- Can implement any routing logic
- Works at Layer 3 (IP level)
Disadvantages:
- Requires kernel privileges
- Complex implementation
- OS-specific (tun on Linux/macOS, TAP on Windows)
RECOMMENDED APPROACH FOR ZTNA:
------------------------------
Combine methods:
1. SOCKS5 proxy on 127.0.0.1:1080 (for apps that support it)
2. PAC file for browser configuration (split-tunnel rules)
3. DNS interception at OS level (prevent leaks)
4. Optional: TUN interface for full control (enterprise deployments)
+--------------------------------------------------------------------------+
HTTP/2 Multiplexing for Tunnel Efficiency
When a user opens 10 browser tabs to internal apps, you don’t want 10 separate mTLS handshakes. HTTP/2 solves this with stream multiplexing.
+--------------------------------------------------------------------------+
| HTTP/2 Multiplexing for Tunnels |
+--------------------------------------------------------------------------+
THE PROBLEM WITHOUT MULTIPLEXING:
---------------------------------
Tab 1 (jira.internal) ----[ mTLS Handshake 1 ]----> Gateway
Tab 2 (gitlab.internal) ----[ mTLS Handshake 2 ]----> Gateway
Tab 3 (wiki.internal) ----[ mTLS Handshake 3 ]----> Gateway
Tab 4 (jira.internal) ----[ mTLS Handshake 4 ]----> Gateway
...
Each handshake: ~100-300ms
10 tabs = 1-3 seconds of latency!
WITH HTTP/2 MULTIPLEXING:
-------------------------
Tab 1 ─┐
Tab 2 ─┼── [ Single mTLS Tunnel ] ──[ Multiplexed Streams ]──> Gateway
Tab 3 ─┤ | |
Tab 4 ─┘ | |
v v
Stream 1 (jira:443) ╔══════════════════════════════╗
Stream 2 (gitlab:443) ║ HTTP/2 Connection ║
Stream 3 (wiki:443) ║ - Stream 1: jira ║
Stream 4 (jira:443) ║ - Stream 2: gitlab ║
║ - Stream 3: wiki ║
║ - Stream 4: jira (reuse) ║
╚══════════════════════════════╝
One handshake: ~100-300ms
10 tabs = still ~100-300ms!
+--------------------------------------------------------------------------+
| HTTP/2 Stream Structure |
+--------------------------------------------------------------------------+
+-----------------------------------------------+
| HTTP/2 Connection |
+-----------------------------------------------+
| Stream 1 | Stream 2 | Stream 3 | ... |
| (jira) | (gitlab) | (wiki) | |
+------------+------------+------------+--------+
| | | | |
| HEADERS | HEADERS | HEADERS | |
| DATA | DATA | DATA | |
| DATA | DATA | HEADERS | |
| END_STREAM | DATA | DATA | |
| | END_STREAM | END_STREAM | |
+-----------------------------------------------+
All streams share ONE TCP connection
Frames are interleaved on the wire
+--------------------------------------------------------------------------+
| Head-of-Line Blocking Problem |
+--------------------------------------------------------------------------+
HTTP/2 still uses TCP, which has head-of-line blocking:
[ Stream 1 Frame ] [ Stream 2 Frame ] [ X Packet Loss ] [ Stream 3 Frame ]
|
v
TCP retransmits before delivering Stream 3
Even though Stream 3's packets arrived!
SOLUTION: HTTP/3 (QUIC)
-----------------------
QUIC uses UDP with per-stream reliability:
[ Stream 1 ] ────────> Independent delivery
[ Stream 2 ] ────────> Independent delivery
[ Stream 3 ] ────────> Independent delivery
Packet loss in Stream 1 doesn't block Stream 2 or 3.
For ZTNA: HTTP/3 is ideal but complex.
Start with HTTP/2, consider QUIC for production.
+--------------------------------------------------------------------------+
| Implementing Multiplexing in the Tunnel |
+--------------------------------------------------------------------------+
ARCHITECTURE:
Client Agent:
+------------------------------------------------------------------+
| |
| [ SOCKS5 Server ] |
| | |
| v |
| [ Stream Manager ] |
| | |
| +-- Stream 1: jira.internal:443 -> gRPC/HTTP2 Stream 1 |
| +-- Stream 2: gitlab.internal:443 -> gRPC/HTTP2 Stream 2 |
| +-- Stream 3: wiki.internal:443 -> gRPC/HTTP2 Stream 3 |
| | |
| v |
| [ HTTP/2 Client ] ---- mTLS ----> Gateway |
| |
+------------------------------------------------------------------+
PSEUDO-CODE (Go):
// Create HTTP/2 transport
transport := &http2.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
},
}
// Reuse connection for all streams
client := &http.Client{Transport: transport}
func handleSOCKS5Connection(conn net.Conn) {
target := parseSocks5Request(conn)
// Use HTTP/2 CONNECT for each stream
req, _ := http.NewRequest("CONNECT",
fmt.Sprintf("https://gateway.corp.com/tunnel/%s", target), nil)
resp, _ := client.Do(req)
// Bidirectional copy
go io.Copy(conn, resp.Body)
io.Copy(resp.Body, conn)
}
+--------------------------------------------------------------------------+
Identity Propagation Through the Tunnel
The tunnel must carry the user’s identity to the gateway so the gateway can inject identity headers into requests to internal services.
+--------------------------------------------------------------------------+
| Identity Flow Through ZTNA Tunnel |
+--------------------------------------------------------------------------+
IDENTITY SOURCES:
-----------------
1. User Identity (from certificate):
- Common Name: douglas@corp.com
- SPIFFE ID: spiffe://corp.com/user/douglas
2. Device Identity (from certificate or attestation):
- Device ID: macbook-42
- Device Posture: SECURE / AT_RISK
3. Session Context:
- Client IP: 203.0.113.42
- Session ID: sess-abc123
- Connection Time: 2024-12-27T10:00:00Z
IDENTITY PROPAGATION:
---------------------
[ User's Browser ]
|
| Request: GET https://jira.internal/dashboard
|
v
[ ZTNA Client Agent ]
|
| Wrap in tunnel with identity:
| - Identity: douglas@corp.com
| - Device: macbook-42
| - Posture: SECURE
|
v
[ mTLS Tunnel ]
|
| TLS Client Certificate contains:
| - CN=douglas@corp.com
| - SAN URI=spiffe://corp.com/user/douglas
| - SAN URI=spiffe://corp.com/device/macbook-42
|
v
[ ZTNA Gateway (Project 1 Proxy) ]
|
| Extract identity from TLS cert
| Check policy (Project 2) for this user+device+app
| Inject headers:
| X-ZT-Identity: douglas@corp.com
| X-ZT-Device: macbook-42
| X-ZT-Posture: SECURE
| X-ZT-Session: sess-abc123
|
v
[ jira.internal ]
|
| Receives request with identity headers
| No login required - identity already verified!
|
v
[ Response flows back through tunnel ]
+--------------------------------------------------------------------------+
| Header Injection by Gateway |
+--------------------------------------------------------------------------+
The gateway (Project 1) does the actual header injection:
INCOMING REQUEST (from tunnel):
+------------------------------------------+
| CONNECT jira.internal:443 HTTP/2 |
| X-ZT-Client-IP: 203.0.113.42 |
| (TLS cert contains user identity) |
+------------------------------------------+
AFTER TERMINATING mTLS:
+------------------------------------------+
| Gateway extracts from cert: |
| - Subject CN: douglas@corp.com |
| - SPIFFE ID: spiffe://corp.com/user/... |
+------------------------------------------+
FORWARDED TO INTERNAL SERVICE:
+------------------------------------------+
| GET /dashboard HTTP/1.1 |
| Host: jira.internal |
| X-ZT-Identity: douglas@corp.com |
| X-ZT-Roles: admin,developer |
| X-ZT-Device: macbook-42 |
| X-ZT-Posture: SECURE |
| X-ZT-Session: sess-abc123 |
| X-ZT-Verified: true |
| X-Forwarded-For: 203.0.113.42 |
+------------------------------------------+
+--------------------------------------------------------------------------+
| Making Internal Apps "Login-Free" |
+--------------------------------------------------------------------------+
BEFORE ZTNA:
1. User connects VPN
2. User opens jira.internal
3. Jira shows login page
4. User enters username/password
5. User accesses dashboard
AFTER ZTNA:
1. User has ZTNA agent running
2. User opens jira.internal
3. ZTNA tunnels request with identity
4. Jira receives X-ZT-Identity header
5. Jira trusts header (from known gateway)
6. User directly sees dashboard - NO LOGIN!
INTERNAL APP CONFIGURATION:
Jira (or any internal app) must:
1. Trust the X-ZT-* headers from the gateway
2. Map X-ZT-Identity to internal user
3. Skip normal authentication when headers present
Security: App must ONLY accept these headers from
the gateway. Usually done via network rules or
checking that request came from gateway IP.
+--------------------------------------------------------------------------+
Project Specification
Functional Requirements
| ID | Requirement | Acceptance Criteria |
|---|---|---|
| FR-1 | Accept SOCKS5 connections on localhost | Listen on 127.0.0.1:1080, respond to SOCKS5 handshake |
| FR-2 | Route based on domain whitelist | Match *.internal, *.corp.com; bypass all others |
| FR-3 | Establish mTLS tunnel to gateway | Connect with client certificate, verify gateway cert |
| FR-4 | Tunnel traffic through gateway | Forward TCP bytes bidirectionally |
| FR-5 | Multiplex multiple connections | Use HTTP/2 or gRPC for stream multiplexing |
| FR-6 | Prevent DNS leaks | Intercept DNS for internal domains |
| FR-7 | Log all routing decisions | Show tunnel/bypass decisions in terminal |
| FR-8 | Load whitelist from config file | YAML configuration for domains and gateway URL |
| FR-9 | Graceful shutdown | Complete active streams before exit |
| FR-10 | Pass identity through tunnel | Include user/device info in tunnel metadata |
Non-Functional Requirements
| ID | Requirement | Target |
|---|---|---|
| NFR-1 | Connection latency overhead | < 50ms added latency per connection |
| NFR-2 | Throughput | > 100 Mbps through tunnel |
| NFR-3 | Concurrent connections | Handle 100+ simultaneous streams |
| NFR-4 | Memory usage | < 50MB under normal load |
| NFR-5 | Certificate rotation | Support rotation without connection drops |
| NFR-6 | Reconnection | Auto-reconnect on tunnel failure |
Architecture Diagram
+--------------------------------------------------------------------------+
| ZTNA App Tunnel Architecture |
+--------------------------------------------------------------------------+
[ User's Machine ]
+------------------------------------------------------------------+
| |
| [ Browser ] [ SSH Client ] [ DB Client ] [ Any App ] |
| | | | | |
| v v v v |
| +----------------------------------------------------------+ |
| | ZTNA Client Agent | |
| | | |
| | +------------------+ +-------------------------+ | |
| | | SOCKS5 Server | | Domain Router | | |
| | | 127.0.0.1:1080 | | - Check whitelist | | |
| | | | | - TUNNEL or BYPASS | | |
| | +------------------+ +-------------------------+ | |
| | | | | |
| | v v | |
| | +------------------+ +-------------------------+ | |
| | | Stream Manager | | Direct Connection | | |
| | | (HTTP/2 Mux) | | (for non-internal) | | |
| | +------------------+ +-------------------------+ | |
| | | | | |
| | v | | |
| | +------------------+ | | |
| | | mTLS Client | | | |
| | | (Project 4 cert) | | | |
| | +------------------+ | | |
| | | | | |
| +-----------|-------------------------|--------------------+ |
| | | |
+--------------|-------------------------|-------------------------+
| |
v v
[ Secure mTLS Tunnel ] [ Direct Internet ]
| |
v v
+----------------------+ [ Public Web ]
| ZTNA Gateway |
| (Project 1 Proxy) |
| |
| - Terminates mTLS |
| - Injects identity |
| - Checks policy (P2) |
+----------+-----------+
|
v
+----------------------+
| Internal Services |
| - jira.internal |
| - gitlab.internal |
| - wiki.internal |
+----------------------+
+--------------------------------------------------------------------------+
Component Breakdown
| Component | Responsibility | Key APIs |
|---|---|---|
| Config Loader | Parse YAML config, load domain whitelist | LoadConfig(path) -> Config |
| SOCKS5 Server | Accept SOCKS5 connections, parse requests | Listen(), Accept(), HandleConnection() |
| Domain Router | Decide tunnel vs bypass based on domain | ShouldTunnel(domain) -> bool |
| Stream Manager | Multiplex streams over HTTP/2 | CreateStream(target) -> Stream |
| mTLS Client | Manage mTLS connection to gateway | Connect(), Reconnect() |
| DNS Interceptor | Capture DNS for internal domains | InterceptDNS(), ResolveThroughTunnel() |
| Identity Provider | Load and provide user/device identity | GetIdentity() -> Identity |
Solution Architecture
Configuration Schema
# corp-apps.yaml
gateway:
url: "https://gateway.corp.com:8443"
certificate: "/path/to/client.crt"
key: "/path/to/client.key"
ca: "/path/to/ca.crt"
identity:
user: "douglas@corp.com"
device: "macbook-42"
tunneled_domains:
- "*.internal"
- "*.corp.com"
- "jira.internal"
- "gitlab.internal"
- "wiki.internal"
- "10.0.0.0/8" # Optional: tunnel by IP range
socks5:
listen: "127.0.0.1:1080"
dns:
intercept: true
internal_resolver: "10.0.0.1:53"
logging:
level: "debug"
format: "json"
Domain Matching Engine
The domain router needs efficient matching for patterns like *.internal:
+--------------------------------------------------------------------------+
| Domain Matching Logic |
+--------------------------------------------------------------------------+
INPUT: "jira.internal"
RULES:
1. Exact match: "jira.internal" matches "jira.internal"
2. Wildcard: "jira.internal" matches "*.internal"
3. Suffix: "jira.internal" matches ".internal"
4. IP Range: "10.0.1.50" matches "10.0.0.0/8"
ALGORITHM:
func ShouldTunnel(domain string) bool {
// 1. Check exact match
if _, exists := exactMatches[domain]; exists {
return true
}
// 2. Check wildcard patterns
for _, pattern := range wildcardPatterns {
if matchWildcard(domain, pattern) {
return true
}
}
// 3. Check IP ranges (if domain is IP)
if ip := net.ParseIP(domain); ip != nil {
for _, cidr := range ipRanges {
if cidr.Contains(ip) {
return true
}
}
}
return false // BYPASS - go direct
}
+--------------------------------------------------------------------------+
Local SOCKS5 Server
The SOCKS5 server is the entry point for client traffic:
+--------------------------------------------------------------------------+
| SOCKS5 Server Flow |
+--------------------------------------------------------------------------+
Accept Connection
|
v
Read Auth Negotiation (VER, NMETHODS, METHODS)
|
v
Send Auth Response (VER, 0x00 = no auth)
|
v
Read Connection Request (VER, CMD, ATYP, ADDR, PORT)
|
+------ CMD = 0x01 (CONNECT) ?
| |
| Yes | No
| v |
| Extract target v
| domain and port Send error (0x07)
| | Close
| v
| ShouldTunnel(domain)?
| |
| Yes | No
| v |
| Create tunnel v
| stream Direct connect
| | to target
| v |
+--------> Send success (0x00)
|
v
Bidirectional copy
(client <-> target)
|
v
Connection closed
+--------------------------------------------------------------------------+
mTLS Connection Manager
Maintains a persistent mTLS connection to the gateway:
+--------------------------------------------------------------------------+
| mTLS Connection Manager |
+--------------------------------------------------------------------------+
+-------------------------------------------------------------+
| Connection Manager |
+-------------------------------------------------------------+
| |
| State: CONNECTED / CONNECTING / DISCONNECTED |
| |
| [ Certificate Manager ] <-- From Project 4 |
| | - Current cert |
| | - Rotation timer |
| | - CA trust store |
| |
| [ HTTP/2 Client ] |
| | - Connection pool (1 connection typically) |
| | - Stream multiplexer |
| | - Keepalive (PING frames) |
| |
| [ Reconnection Logic ] |
| | - Exponential backoff |
| | - Max retries |
| | - Connection health monitoring |
| |
+-------------------------------------------------------------+
API:
func (cm *ConnectionManager) CreateStream(target string) (Stream, error)
func (cm *ConnectionManager) Close() error
func (cm *ConnectionManager) IsHealthy() bool
+--------------------------------------------------------------------------+
Stream Multiplexer
Uses HTTP/2 (or gRPC) to multiplex multiple TCP connections over one mTLS tunnel:
+--------------------------------------------------------------------------+
| Stream Multiplexer Design |
+--------------------------------------------------------------------------+
Protocol Choice: HTTP/2 with CONNECT or gRPC bidirectional streams
OPTION 1: HTTP/2 CONNECT
-------------------------
Client sends:
CONNECT jira.internal:443 HTTP/2
Host: jira.internal:443
X-ZT-Stream-ID: stream-001
X-ZT-Identity: douglas@corp.com
Gateway responds:
HTTP/2 200 OK
X-ZT-Bound-To: jira.internal:443
Then: Raw TCP bytes flow over HTTP/2 DATA frames
OPTION 2: gRPC Bidirectional Stream
-----------------------------------
proto:
service Tunnel {
rpc Connect(stream TunnelData) returns (stream TunnelData);
}
message TunnelData {
oneof message {
ConnectRequest connect = 1;
ConnectResponse connect_response = 2;
DataChunk data = 3;
CloseStream close = 4;
}
}
message ConnectRequest {
string target = 1; // "jira.internal:443"
string identity = 2;
}
RECOMMENDATION:
---------------
Start with HTTP/2 CONNECT:
- Simpler to implement
- Standard protocol
- Gateway (Project 1) can be a standard HTTP/2 proxy
Consider gRPC for:
- Multiple streams per request
- Custom metadata per stream
- Better error handling
+--------------------------------------------------------------------------+
Phased Implementation Guide
Phase 1: Basic SOCKS5 Proxy (Pass-Through)
Goal: Build a functional SOCKS5 proxy that directly connects to any target.
Deliverables:
- SOCKS5 server listening on 127.0.0.1:1080
- Handles SOCKS5 authentication negotiation (no auth)
- Handles CONNECT command for domain and IP targets
- Bidirectional data forwarding
Steps:
- Parse SOCKS5 authentication negotiation
- Parse SOCKS5 connection request
- Resolve target domain (using system DNS)
- Connect to target directly
- Forward data bidirectionally
Verification:
# Start your SOCKS5 proxy
$ ./ztna-tunnel --socks5-only
# Test with curl
$ curl -x socks5h://127.0.0.1:1080 https://example.com
# Should return example.com's HTML
# Test with Firefox (set network.proxy.socks to 127.0.0.1:1080)
Phase 2: Domain-Based Routing (Split Tunnel)
Goal: Route internal domains through the tunnel, bypass others.
Deliverables:
- Load domain whitelist from config file
- Domain matching engine (exact, wildcard, suffix)
- Route matching domains to tunnel stub
- Route non-matching domains directly
Steps:
- Implement config loader (YAML)
- Build domain matcher with wildcard support
- Add routing decision after SOCKS5 request parsing
- For now, return error for tunneled domains (no gateway yet)
Verification:
# Configure whitelist for *.internal
$ ./ztna-tunnel --config ./corp-apps.yaml
# Test tunneled domain (should fail - no gateway yet)
$ curl -x socks5h://127.0.0.1:1080 https://jira.internal
# Expected: Connection refused (tunnel not implemented)
# Test bypassed domain (should work)
$ curl -x socks5h://127.0.0.1:1080 https://example.com
# Expected: Success (direct connection)
Phase 3: mTLS Upstream Connection
Goal: Establish secure mTLS tunnel to the gateway (Project 1).
Deliverables:
- Load client certificate and key
- Connect to gateway with mTLS
- Verify gateway certificate
- Handle connection errors gracefully
Steps:
- Load certificates from config paths
- Configure TLS with client cert and CA trust
- Establish HTTP/2 connection to gateway
- Implement health check (PING/keepalive)
- Implement reconnection with backoff
Verification:
# Start Project 1 gateway with mTLS
$ ./zta-proxy --mtls --listen :8443
# Start tunnel client
$ ./ztna-tunnel --config ./corp-apps.yaml
# Check tunnel connection
# Look for: [INFO] Connected to gateway.corp.com
Phase 4: Tunnel Traffic Through Gateway
Goal: Route internal domain traffic through the mTLS tunnel.
Deliverables:
- Create HTTP/2 CONNECT stream for each connection
- Forward SOCKS5 connection through tunnel stream
- Bidirectional data forwarding through tunnel
- Stream cleanup on connection close
Steps:
- Implement HTTP/2 CONNECT request to gateway
- Map SOCKS5 connection to HTTP/2 stream
- Forward data: client -> tunnel -> gateway -> target
- Forward data: target -> gateway -> tunnel -> client
- Handle stream errors and cleanup
Verification:
# Start Project 1 gateway with backend on 8081
$ ./zta-proxy --backend http://localhost:8081 --mtls
# Start simple backend
$ python3 -m http.server 8081
# Start tunnel
$ ./ztna-tunnel --config ./corp-apps.yaml
# Test tunneled request
$ curl -x socks5h://127.0.0.1:1080 http://localhost.internal:8081/
# Should return Python server directory listing
Phase 5: Identity Injection
Goal: Pass user and device identity through the tunnel.
Deliverables:
- Include identity in tunnel handshake/metadata
- Gateway extracts and injects X-ZT-* headers
- Identity visible in backend logs
Steps:
- Add identity to CONNECT request headers
- Update gateway to extract identity from tunnel
- Gateway injects X-ZT-Identity, X-ZT-Device headers
- Verify headers arrive at backend
Verification:
# Start backend that echoes headers
$ python3 -c "
from http.server import *
class H(SimpleHTTPRequestHandler):
def do_GET(self):
print(self.headers)
self.send_response(200)
self.end_headers()
HTTPServer(('', 8081), H).serve_forever()
"
# Tunnel request - check backend logs for X-ZT-* headers
$ curl -x socks5h://127.0.0.1:1080 http://internal.local:8081/
# Backend should show: X-ZT-Identity: douglas@corp.com
Phase 6: DNS Leak Prevention
Goal: Ensure DNS for internal domains goes through tunnel.
Deliverables:
- Intercept DNS queries at SOCKS5 level
- Forward internal domain DNS through tunnel
- Allow external domain DNS to go direct
Steps:
- Use SOCKS5h (DNS through proxy) mode
- For internal domains, resolve through tunnel
- Implement DNS resolution endpoint on gateway
- Cache DNS responses for performance
Verification:
# Run Wireshark or tcpdump
$ sudo tcpdump -i en0 port 53
# Make tunneled request
$ curl -x socks5h://127.0.0.1:1080 https://jira.internal/
# Check tcpdump - should NOT see jira.internal DNS query
# The 'h' in socks5h means DNS is resolved by the proxy
Phase 7: HTTP/2 Multiplexing Optimization
Goal: Efficiently handle multiple concurrent connections.
Deliverables:
- Single HTTP/2 connection for all streams
- Proper stream lifecycle management
- Connection health monitoring
- Graceful degradation under load
Steps:
- Verify HTTP/2 multiplexing is working
- Add stream tracking and limits
- Implement connection health monitoring
- Handle gateway disconnection gracefully
Verification:
# Open multiple concurrent connections
$ for i in {1..10}; do
curl -x socks5h://127.0.0.1:1080 https://jira.internal/ &
done
wait
# Check tunnel logs - should show:
# - Single mTLS handshake (not 10)
# - 10 streams created on same connection
Testing Strategy
Unit Tests
| Component | Test Case | Expected Result |
|---|---|---|
| SOCKS5 Parser | Valid auth negotiation | Parse correctly |
| SOCKS5 Parser | Invalid version (0x04) | Return error |
| SOCKS5 Parser | CONNECT with domain | Extract domain and port |
| SOCKS5 Parser | CONNECT with IPv4 | Extract IP and port |
| SOCKS5 Parser | CONNECT with IPv6 | Extract IP and port |
| Domain Matcher | Exact match “jira.internal” | Return true |
| Domain Matcher | Wildcard “*.internal” matches “jira.internal” | Return true |
| Domain Matcher | Wildcard “*.internal” doesn’t match “jira.com” | Return false |
| Domain Matcher | IP range “10.0.0.0/8” contains “10.0.1.50” | Return true |
| Config Loader | Valid YAML | Load all fields |
| Config Loader | Missing required field | Return error |
Integration Tests
| Scenario | Setup | Expected Behavior |
|---|---|---|
| Tunneled HTTP | Gateway running, internal backend | Request succeeds, identity headers present |
| Tunneled HTTPS | Gateway with TLS backend | TLS works end-to-end |
| Bypassed traffic | No gateway needed | Direct connection works |
| Gateway down | Gateway offline | Graceful error, retry logic |
| Gateway reconnect | Gateway restarts | Auto-reconnect, pending requests wait |
| Multiple streams | 20 concurrent requests | All succeed, multiplexed |
| Large transfer | 100MB file download | Completes without memory issues |
Security Tests
| Attack Vector | Test Method | Expected Defense |
|---|---|---|
| DNS leak | tcpdump for internal DNS | No internal DNS on external interface |
| Tunnel bypass | Direct connection to internal IP | Connection refused (no route) |
| Identity spoofing | Send fake X-ZT-* headers | Headers stripped by gateway |
| Certificate mismatch | Use cert from different CA | Connection refused |
| Expired certificate | Use expired cert | Connection refused |
| Man-in-the-middle | Intercept tunnel traffic | TLS failure |
Performance Tests
Latency Test:
# Measure overhead
$ time curl -x socks5h://127.0.0.1:1080 https://jira.internal/ping
# Compare to direct (when VPN would be used)
Throughput Test:
# Download large file
$ curl -x socks5h://127.0.0.1:1080 https://internal.local/100mb.bin -o /dev/null
# Should achieve >100Mbps on local network
Concurrency Test:
# 100 concurrent connections
$ hey -n 1000 -c 100 -x socks5h://127.0.0.1:1080 https://jira.internal/
Common Pitfalls and Debugging
Pitfall 1: DNS Leaks
Symptom: Internal domain names visible on public DNS logs or tcpdump
Cause: Using socks5:// instead of socks5h://, or DNS resolution happening before SOCKS5
Debugging:
$ sudo tcpdump -i en0 port 53
# Make request and watch for internal domain queries
Solution:
- Always use
socks5h://(DNS through proxy) - Implement DNS interception at OS level for non-SOCKS apps
- Configure browser to use remote DNS resolution
Pitfall 2: Certificate Errors
Symptom: “certificate signed by unknown authority” or similar TLS errors
Cause: CA not trusted, certificate expired, or wrong certificate chain
Debugging:
# Check certificate details
$ openssl s_client -connect gateway.corp.com:8443 \
-cert client.crt -key client.key -CAfile ca.crt
# Verify certificate chain
$ openssl verify -CAfile ca.crt client.crt
Solution:
- Verify CA certificate is correct
- Check certificate expiration dates
- Ensure full certificate chain is provided
Pitfall 3: Browser Proxy Configuration
Symptom: Browser doesn’t use the SOCKS5 proxy
Cause: Browser settings override, PAC file issues, or extension conflicts
Debugging:
# Test with curl first (known to work)
$ curl -x socks5h://127.0.0.1:1080 https://jira.internal/
# Check browser proxy settings
# Chrome: chrome://settings/system -> Proxy settings
# Firefox: about:preferences -> Network Settings
Solution:
- Use a PAC file for automatic configuration
- Disable proxy extensions that might conflict
- For testing, use Firefox’s built-in SOCKS5 settings
Pitfall 4: Non-HTTP Protocol Issues
Symptom: SSH or database connections fail through tunnel
Cause: Protocol expects specific behavior that tunnel doesn’t handle
Debugging:
# Test SSH through tunnel
$ ssh -o ProxyCommand='nc -x 127.0.0.1:1080 %h %p' internal-server
# Check for SOCKS5 UDP support (needed for some protocols)
Solution:
- Ensure SOCKS5 CONNECT command is fully implemented
- For UDP protocols, implement SOCKS5 UDP ASSOCIATE (0x03)
- Some protocols may need additional tunnel configuration
Pitfall 5: Connection Pooling Issues
Symptom: “too many open files” or connection refused after many requests
Cause: Not properly closing streams or connections
Debugging:
# Check open connections
$ lsof -i -P | grep ztna-tunnel
# Check for connection leaks
$ netstat -an | grep 1080
Solution:
- Implement proper stream cleanup on completion
- Add connection limits
- Implement idle timeout for unused streams
Pitfall 6: Multiplexing Not Working
Symptom: Multiple mTLS handshakes visible for concurrent requests
Cause: HTTP/2 not properly configured, or new connection per request
Debugging:
# Check HTTP/2 is negotiated
$ curl -v --http2 https://gateway.corp.com:8443/
# Wireshark: filter for TLS handshakes
# Should see only 1 handshake for multiple streams
Solution:
- Ensure HTTP/2 is enabled on both client and gateway
- Reuse http.Client with connection pooling
- Check for transport configuration that forces HTTP/1.1
Extensions and Challenges
Extension 1: UDP Support (SOCKS5 UDP ASSOCIATE)
Implement the SOCKS5 UDP ASSOCIATE command for protocols like DNS and some games.
Challenge: UDP doesn’t have connections - you’ll need to relay packets.
RFC Reference: RFC 1928, Section 7
Extension 2: Mobile Client (iOS/Android)
Build a mobile ZTNA client using platform-specific networking APIs.
iOS: Network Extension framework for VPN-like functionality Android: VpnService API for creating a virtual network interface
Extension 3: Browser Extension
Instead of system proxy, build a browser extension that intercepts requests.
Advantage: Works without system-level changes Challenge: Limited to browser traffic only
Extension 4: QUIC/HTTP3 Support
Replace HTTP/2 with HTTP/3 (QUIC) for better performance.
Advantage: No head-of-line blocking, faster handshakes Challenge: QUIC is more complex to implement
Extension 5: Kubernetes Sidecar Deployment
Deploy the ZTNA client as a sidecar proxy in Kubernetes pods.
Use Case: Outbound traffic from pods to internal services Integration: Works with service mesh concepts
Extension 6: Policy Integration (Project 2)
Have the tunnel check with the Policy Decision Engine before tunneling.
Flow: User requests app -> Tunnel asks PDP -> PDP approves/denies
Real-World Connections
How Commercial Products Implement This
| Product | Architecture | Key Differentiator |
|---|---|---|
| Cloudflare Access / WARP | Global edge PoPs, WARP client | Anycast routing, massive scale |
| Zscaler ZPA | App Connectors inside network | Outbound-only connections (no inbound) |
| Google BeyondCorp Remote Access | IAP (Identity-Aware Proxy) | Deep GCP integration |
| Tailscale | WireGuard mesh network | Peer-to-peer where possible |
| Palo Alto Prisma Access | Cloud-delivered SASE | Full security stack |
What Makes Commercial Products Different
- Global Points of Presence (PoPs)
- Your tunnel: Single gateway
- Commercial: 200+ PoPs worldwide for low latency
- Mobile Device Management (MDM)
- Your tunnel: Basic device identity
- Commercial: Full MDM integration, device posture checks
- Threat Prevention
- Your tunnel: Tunneling only
- Commercial: Inline malware scanning, DLP, CASB
- High Availability
- Your tunnel: Single gateway
- Commercial: Automatic failover, load balancing
- Compliance Features
- Your tunnel: Basic logging
- Commercial: Audit trails, compliance reports, SIEM integration
Interview Questions
Conceptual Questions
Q1: What is ZTNA and how does it differ from a VPN?
Expected Answer:
- VPN connects user’s device to a NETWORK
- ZTNA connects user to a specific APPLICATION
- VPN: “You’re inside the network, access anything”
- ZTNA: “You can access jira.internal, nothing else”
- Attack surface with VPN: entire network
- Attack surface with ZTNA: specific apps only
Q2: Explain split tunneling and why it’s the default for Zero Trust.
Expected Answer:
- Split tunnel: Only internal traffic goes through tunnel
- Full tunnel: All traffic goes through VPN
- Split tunnel advantages:
- Better performance (no VPN hop for internet)
- Less bandwidth on tunnel
- Privacy (personal traffic not inspected)
- Zero Trust doesn’t care about protecting users from the internet
- It cares about protecting apps from users
Q3: What is a DNS leak and why is it dangerous?
Expected Answer:
- DNS query for internal domain (jira.internal) goes to public DNS
- Public DNS now knows internal service names
- This reveals:
- Company infrastructure details
- Naming conventions
- Which services exist
- Attackers can use this for reconnaissance
- Prevention: Route internal DNS through tunnel
Q4: Why use SOCKS5 instead of HTTP proxy for ZTNA?
Expected Answer:
- SOCKS5 is Layer 4 (Transport)
- HTTP proxy is Layer 7 (Application)
- SOCKS5 can tunnel ANY TCP/UDP traffic
- HTTP proxy only works for HTTP/HTTPS
- ZTNA needs to support: SSH, databases, RDP, custom protocols
- SOCKS5 is protocol-agnostic
Q5: How does mTLS secure the tunnel?
Expected Answer:
- mTLS = Mutual TLS = both sides authenticate
- Client proves identity with certificate
- Server proves identity with certificate
- Both are verified by trusted CA
- If attacker intercepts tunnel: Can’t impersonate either side
- If attacker compromises client: Certificate revoked, no access
- Connection to Project 4 concepts
Technical Deep-Dives
Q6: Walk through what happens when a user accesses jira.internal through your tunnel.
Expected Answer:
- Browser configured to use SOCKS5 proxy at 127.0.0.1:1080
- Browser sends SOCKS5 CONNECT request for jira.internal:443
- ZTNA client checks domain against whitelist - matches *.internal
- Client creates HTTP/2 CONNECT stream to gateway
- Gateway verifies mTLS client certificate
- Gateway establishes connection to jira.internal
- Gateway injects X-ZT-Identity headers
- Response flows back through tunnel to browser
- All on a single mTLS connection (multiplexed)
Q7: How would you handle the case where the gateway becomes unreachable?
Expected Answer:
- Detect connection failure (read/write error, timeout)
- Mark connection as unhealthy
- Queue new requests (don’t fail immediately)
- Attempt reconnection with exponential backoff
- After reconnection, resume queued requests
- If max retries exceeded, fail pending requests gracefully
- Show user-friendly error: “Cannot reach internal services”
- Continue bypassed traffic normally
Q8: How do you prevent the tunnel from becoming a vector for lateral movement?
Expected Answer:
- Tunnel only allows specific domains, not IP ranges
- Each application requires explicit authorization
- Gateway (Project 1) enforces policy per-request
- Policy engine (Project 2) can require re-authentication
- Device posture (Project 5) blocks compromised devices
- No ability to scan network - only reach authorized apps
- Even if agent is compromised, blast radius is limited
Q9: What are the tradeoffs between HTTP/2 and gRPC for the tunnel protocol?
Expected Answer:
- HTTP/2 CONNECT:
- Standard protocol
- Gateway can be any HTTP/2 proxy
- Simpler to implement
- Works with existing tooling
- gRPC:
- Better error handling (status codes)
- Easier streaming semantics
- Built-in serialization
- More flexibility for metadata
- Recommendation: Start with HTTP/2 for simplicity
Q10: If you could only implement one DNS leak prevention method, which would it be?
Expected Answer:
- Use SOCKS5h (DNS resolved by proxy)
- Why: It works at the application level
- Browser resolves DNS through the proxy, not locally
- No special OS privileges needed
- Covers the most common case (browser traffic)
- Limitation: Doesn’t protect non-SOCKS-aware apps
- For full protection: Also implement OS-level DNS interception
Resources and Self-Assessment
Essential Reading
| Resource | Focus | Chapter/Section |
|---|---|---|
| “TCP/IP Illustrated, Volume 1” by Stevens | TCP, UDP, and DNS fundamentals | Chapters 17-18 (TCP), Chapter 14 (DNS) |
| “Zero Trust Networks” by Gilman & Barth | ZTNA architecture | Chapters 9-10 |
| “Computer Networks” by Tanenbaum | Networking fundamentals, VPNs | Section 8.8 (VPNs) |
| “TCP/IP Sockets in C” by Donahoo | Socket programming | Full book |
| RFC 1928 | SOCKS5 Protocol | Complete RFC |
| RFC 7230 | HTTP/1.1 Message Syntax | CONNECT method |
| RFC 9113 | HTTP/2 | Streams and multiplexing |
RFCs to Reference
- RFC 1928 - SOCKS Protocol Version 5
- RFC 1929 - Username/Password Authentication for SOCKS5
- RFC 7230 - HTTP/1.1 Message Syntax (CONNECT method)
- RFC 9113 - HTTP/2
- RFC 8446 - TLS 1.3
- RFC 9000 - QUIC (if implementing HTTP/3)
Tools for Development and Testing
- curl - Testing SOCKS5 with
-x socks5h:// - Wireshark - Analyzing TLS handshakes and HTTP/2 streams
- tcpdump - Detecting DNS leaks
- openssl s_client - Testing mTLS connections
- Firefox - Built-in SOCKS5 proxy settings
- Proxifier (Windows/macOS) - Force apps through proxy
Self-Assessment Checklist
Conceptual Understanding:
- I can explain why ZTNA is more secure than VPN
- I can describe split tunneling and its security implications
- I understand the SOCKS5 protocol at the packet level
- I can explain DNS leaks and prevention methods
- I understand HTTP/2 multiplexing and its benefits
- I can articulate how mTLS secures the tunnel
Implementation Verification:
- My SOCKS5 server correctly handles authentication negotiation
- My SOCKS5 server correctly parses CONNECT requests
- Domain-based routing correctly splits tunnel/bypass traffic
- mTLS connection to gateway establishes successfully
- Traffic flows bidirectionally through the tunnel
- Identity is passed through tunnel and injected by gateway
- DNS for internal domains does NOT leak
Security Verification:
- No internal DNS queries visible on public network
- Direct connections to internal IPs are blocked
- Identity headers cannot be spoofed by client
- Invalid certificates are rejected
- Connection failures are handled gracefully
Production Readiness:
- Configuration is loaded from file
- Logs are structured and informative
- Reconnection logic handles gateway restarts
- Performance meets latency and throughput targets
- Memory usage is bounded under load
Conclusion
This project brings together networking fundamentals, cryptography, and Zero Trust principles to build a modern replacement for VPNs. By building your own ZTNA App Tunnel, you understand:
- Why network-level access (VPN) is dangerous
- How application-level access (ZTNA) limits blast radius
- The details of SOCKS5 protocol for protocol-agnostic tunneling
- The critical importance of DNS leak prevention
- How mTLS provides bidirectional authentication
- Why split tunneling is the Zero Trust default
The skills from this project apply directly to:
- Enterprise security architecture
- Service mesh implementations
- Modern access control design
- Security engineering interviews at ZTNA companies
After completing this project, you’ll understand how products like Cloudflare Access, Zscaler ZPA, and Tailscale work under the hood.
This guide was expanded from ZERO_TRUST_ARCHITECTURE_DEEP_DIVE.md. For the complete learning path, see the project index.