Created
February 3, 2026 20:08
-
-
Save Vaisakhkm2625/faa79139d3a1fe9fca26b8a26510ba79 to your computer and use it in GitHub Desktop.
run mulitiple tailscale instances in same system, by creating virutal networks namespaces in linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Creates an isolated network namespace with: | |
| - veth pair + NAT for real internet access | |
| - simple HTTP server | |
| - tailscaled in userspace-networking mode | |
| - joins Tailscale using ephemeral tagged auth key | |
| Requires: root privileges, tailscale installed, requests library, iptables | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import subprocess | |
| import requests | |
| import argparse | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| # ──────────────────────────────────────────────── | |
| # Configuration - from environment | |
| # ──────────────────────────────────────────────── | |
| CLIENT_ID = os.getenv("CLIENT_ID") | |
| CLIENT_SECRET = os.getenv("CLIENT_SECRET") | |
| TAILNET = os.getenv("TAILNET") # example: example.com | |
| if not all([CLIENT_ID, CLIENT_SECRET, TAILNET]): | |
| print("Missing required environment variables:") | |
| print(" CLIENT_ID, CLIENT_SECRET, TAILNET") | |
| sys.exit(1) | |
| SERVER_PORT = 8080 # port the HTTP server listens on | |
| TAG = "tag:kcuser1" # ← change to your tag | |
| # Networking config inside namespace | |
| NS_IPV4 = "192.168.200.2/24" | |
| HOST_IPV4 = "192.168.200.1/24" | |
| VETH_NS = "veth-ns" # interface name inside namespace | |
| VETH_HOST = "veth-host" # interface name on host | |
| # ──────────────────────────────────────────────── | |
| # Tailscale Auth Key generation | |
| # ──────────────────────────────────────────────── | |
| def get_auth_key(): | |
| """Fetches an OAuth token and creates a tagged ephemeral Auth Key.""" | |
| token_url = "https://login.tailscale.com/api/v2/oauth/token" | |
| token_data = { | |
| "client_id": CLIENT_ID, | |
| "client_secret": CLIENT_SECRET, | |
| "grant_type": "client_credentials" | |
| } | |
| token_resp = requests.post(token_url, data=token_data) | |
| token_resp.raise_for_status() | |
| access_token = token_resp.json()["access_token"] | |
| key_url = f"https://api.tailscale.com/api/v2/tailnet/{TAILNET}/keys" | |
| payload = { | |
| "capabilities": { | |
| "devices": { | |
| "create": { | |
| "reusable": False, | |
| "ephemeral": True, | |
| "tags": [TAG] | |
| } | |
| } | |
| }, | |
| "expirySeconds": 3600 # 1 hour | |
| } | |
| headers = {"Authorization": f"Bearer {access_token}"} | |
| key_resp = requests.post(key_url, json=payload, headers=headers) | |
| key_resp.raise_for_status() | |
| key_data = key_resp.json() | |
| auth_key = key_data.get("key") | |
| if not auth_key: | |
| raise RuntimeError("Failed to get auth key from response") | |
| print(f"Generated auth key (ephemeral, tagged {TAG}): {auth_key[:12]}...") | |
| return auth_key | |
| # ──────────────────────────────────────────────── | |
| # Namespace + Networking Setup | |
| # ──────────────────────────────────────────────── | |
| def setup_namespace_with_internet(ns_name: str): | |
| """Create namespace + veth pair + NAT + default route""" | |
| print(f"Setting up namespace {ns_name} with internet access...") | |
| main_interface = get_default_interface() | |
| if not main_interface: | |
| raise RuntimeError("Could not detect default network interface") | |
| print(f"Detected main interface: {main_interface}") | |
| cmds = [ | |
| # Create namespace | |
| ["ip", "netns", "add", ns_name], | |
| # Create veth pair | |
| ["ip", "link", "add", VETH_HOST, "type", "veth", "peer", "name", VETH_NS], | |
| # Move namespace end into namespace | |
| ["ip", "link", "set", VETH_NS, "netns", ns_name], | |
| # Configure IPs | |
| ["ip", "netns", "exec", ns_name, "ip", "addr", "add", NS_IPV4, "dev", VETH_NS], | |
| ["ip", "netns", "exec", ns_name, "ip", "link", "set", VETH_NS, "up"], | |
| ["ip", "addr", "add", HOST_IPV4, "dev", VETH_HOST], | |
| ["ip", "link", "set", VETH_HOST, "up"], | |
| # Enable loopback inside namespace | |
| ["ip", "netns", "exec", ns_name, "ip", "link", "set", "lo", "up"], | |
| # Enable IP forwarding on host | |
| ["sysctl", "-w", "net.ipv4.ip_forward=1"], | |
| # Add NAT (masquerade) | |
| ["iptables", "-t", "nat", "-A", "POSTROUTING", | |
| "-s", "192.168.200.0/24", | |
| "-o", main_interface, "-j", "MASQUERADE"], | |
| # Default route inside namespace | |
| ["ip", "netns", "exec", ns_name, "ip", "route", "add", "default", "via", "192.168.200.1"], | |
| ] | |
| for cmd in cmds: | |
| print(f"Running: {' '.join(cmd)}") | |
| subprocess.run(cmd, check=True) | |
| def get_default_interface(): | |
| """Find the interface with default route""" | |
| try: | |
| output = subprocess.check_output(["ip", "-4", "route", "show", "default"]).decode() | |
| for line in output.splitlines(): | |
| if "dev" in line: | |
| parts = line.split() | |
| for i, part in enumerate(parts): | |
| if part == "dev": | |
| return parts[i + 1] | |
| return None | |
| except Exception: | |
| return None | |
| def start_http_server_in_namespace(ns_name: str, port: int = SERVER_PORT): | |
| """Runs a very simple HTTP server inside the namespace""" | |
| print(f"Starting HTTP server in namespace {ns_name} on port {port}") | |
| server_code = f""" | |
| import http.server | |
| import socketserver | |
| print("Simple HTTP server running on port {port} inside namespace") | |
| with socketserver.TCPServer(("", {port}), http.server.SimpleHTTPRequestHandler) as httpd: | |
| httpd.serve_forever() | |
| """ | |
| script_path = f"/tmp/simple-server-{ns_name}.py" | |
| with open(script_path, "w") as f: | |
| f.write(server_code) | |
| proc = subprocess.Popen([ | |
| "ip", "netns", "exec", ns_name, | |
| "python3", script_path | |
| ]) | |
| return proc, script_path | |
| def join_tailscale_in_namespace(ns_name: str, auth_key: str): | |
| """Runs tailscaled + tailscale up inside the namespace""" | |
| print(f"Joining {ns_name} to tailnet...") | |
| state_dir = f"/var/lib/tailscale/{ns_name}" | |
| socket_path = f"/tmp/tailscale-{ns_name}.sock" | |
| Path(state_dir).mkdir(parents=True, exist_ok=True) | |
| tailscaled_proc = subprocess.Popen([ | |
| "ip", "netns", "exec", ns_name, | |
| "tailscaled", | |
| "--tun=userspace-networking", | |
| f"--state={state_dir}/tailscaled.state", | |
| f"--socket={socket_path}", | |
| "--verbose=1" | |
| ]) | |
| # Give tailscaled time to initialize | |
| time.sleep(4) | |
| up_result = subprocess.run([ | |
| "ip", "netns", "exec", ns_name, | |
| "tailscale", | |
| "--socket", socket_path, | |
| "up", | |
| f"--authkey={auth_key}", | |
| "--accept-routes", | |
| "--accept-dns=false", | |
| f"--hostname={ns_name}", | |
| ], capture_output=True, text=True) | |
| print("tailscale up output:") | |
| print(up_result.stdout) | |
| if up_result.returncode != 0: | |
| print("tailscale up failed:") | |
| print(up_result.stderr) | |
| raise RuntimeError("Failed to join tailnet") | |
| print(f"Namespace {ns_name} joined to tailnet!") | |
| return tailscaled_proc | |
| def cleanup_namespace(ns_name: str): | |
| """Basic cleanup — delete namespace and remove iptables rule""" | |
| print(f"Cleaning up namespace {ns_name}") | |
| # Remove NAT rule | |
| subprocess.run([ | |
| "iptables", "-t", "nat", "-D", "POSTROUTING", | |
| "-s", "192.168.200.0/24", "-j", "MASQUERADE" | |
| ], check=False) | |
| # Delete namespace (also stops processes inside) | |
| subprocess.run(["ip", "netns", "delete", ns_name], check=False) | |
| # ──────────────────────────────────────────────── | |
| # Main logic | |
| # ──────────────────────────────────────────────── | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--name", default="ns-test", help="Name of the namespace") | |
| parser.add_argument("--cleanup", action="store_true", help="Cleanup after demo") | |
| args = parser.parse_args() | |
| ns_name = args.name | |
| server_proc = None | |
| tailscaled_proc = None | |
| try: | |
| # 1. Generate auth key | |
| auth_key = get_auth_key() | |
| # 2. Setup namespace + networking + internet | |
| setup_namespace_with_internet(ns_name) | |
| # 3. Start HTTP server | |
| server_proc, _ = start_http_server_in_namespace(ns_name) | |
| # 4. Join Tailscale | |
| tailscaled_proc = join_tailscale_in_namespace(ns_name, auth_key) | |
| print("\n" + "="*70) | |
| print(f"Success! Namespace '{ns_name}' is running") | |
| print(f"→ HTTP server running inside on port {SERVER_PORT}") | |
| print(f"→ Find Tailscale IP:") | |
| print(f" sudo ip netns exec {ns_name} tailscale ip -4") | |
| print("Press Ctrl+C to stop...\n") | |
| while True: | |
| time.sleep(1) | |
| except KeyboardInterrupt: | |
| print("\nShutting down...") | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| finally: | |
| if server_proc: | |
| server_proc.terminate() | |
| if tailscaled_proc: | |
| tailscaled_proc.terminate() | |
| if args.cleanup: | |
| cleanup_namespace(ns_name) | |
| else: | |
| print("\nNamespace still exists. Cleanup manually if needed:") | |
| print(f" sudo ip netns delete {ns_name}") | |
| print(" sudo iptables -t nat -D POSTROUTING -s 192.168.200.0/24 -j MASQUERADE") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment