{"id":47616502,"url":"https://github.com/rajsinghtech/tailbridge","last_synced_at":"2026-04-01T21:30:30.422Z","repository":{"id":345909925,"uuid":"1187805764","full_name":"rajsinghtech/tailbridge","owner":"rajsinghtech","description":"Cross-tailnet TCP bridge using Tailscale VIP services","archived":false,"fork":false,"pushed_at":"2026-03-21T13:23:02.000Z","size":60,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-22T00:06:35.765Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rajsinghtech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-21T07:28:32.000Z","updated_at":"2026-03-21T08:40:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rajsinghtech/tailbridge","commit_stats":null,"previous_names":["rajsinghtech/tailbridge"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/rajsinghtech/tailbridge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajsinghtech%2Ftailbridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajsinghtech%2Ftailbridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajsinghtech%2Ftailbridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajsinghtech%2Ftailbridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rajsinghtech","download_url":"https://codeload.github.com/rajsinghtech/tailbridge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rajsinghtech%2Ftailbridge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31292314,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T21:15:39.731Z","status":"ssl_error","status_checked_at":"2026-04-01T21:15:34.046Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-01T21:30:30.015Z","updated_at":"2026-04-01T21:30:30.412Z","avatar_url":"https://github.com/rajsinghtech.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tailbridge\n\nCross-tailnet TCP bridge. Makes machines on one Tailscale tailnet reachable from another using [VIP services](https://tailscale.com/kb/1552/tailscale-services).\n\n## Why tailbridge?\n\nSeparate tailnets can't route to each other — each is its own trust boundary with its own ACLs, keys, and DNS. tailbridge proxies TCP traffic across the boundary with full name resolution. No firewall rules, no VPN peering, no IP conflicts.\n\n## Who uses this\n\n- Platform/SRE teams bridging prod vs. dev/staging or multi-account org tailnets\n- MSPs exposing customer services into their ops tailnet without full network membership\n- Compliance environments (OT/IoT, HIPAA) sharing specific ports across isolated tailnets\n- Cross-org projects scoping temporary, port-limited access into a partner's tailnet\n\n```mermaid\nflowchart TD\n    subgraph src[\"Tailnet 1\"]\n        machines[\"web-1\\ndb-1\\n(your servers)\"]\n    end\n\n    subgraph tb[\"tailbridge\"]\n        direction LR\n        discover[\"discovers\\nnew machines\"]\n        proxy[\"forwards\\ntraffic\"]\n    end\n\n    subgraph dst[\"Tailnet 2\"]\n        access[\"web-1.tailnet1.ts.net\\ndb-1.tailnet1.ts.net\"]\n        client[\"client\"]\n    end\n\n    machines --\u003e discover\n    discover --\u003e|\"creates access points\"| access\n    client --\u003e|\"connects by name\"| access\n    access --\u003e proxy\n    proxy --\u003e machines\n```\n\ntailbridge runs two [tsnet](https://pkg.go.dev/tailscale.com/tsnet) servers in one process, each joined to a different tailnet. It discovers machines by tag on the source tailnet, creates a VIP service for each on the destination tailnet, and forwards TCP connections through the source tailnet's WireGuard tunnel. An integrated DNS server resolves original FQDNs to VIP addresses, with automatic split-DNS configuration so clients use original hostnames transparently.\n\n## Install\n\n```sh\nbrew install rajsinghtech/tap/tailbridge\n```\n\nOr grab a binary from [releases](https://github.com/rajsinghtech/tailbridge/releases), or use Docker:\n\n```sh\ndocker pull ghcr.io/rajsinghtech/tailbridge:latest\n```\n\n## Quickstart\n\nCreate OAuth clients on both tailnets with `devices:read`, `dns:write` scopes.\n\n`config.yaml`:\n\n```yaml\nbridge:\n  tailnets:\n    tailnet1:\n      clientId: ${TAILNET1_CLIENT_ID}\n      clientSecret: ${TAILNET1_CLIENT_SECRET}\n      tags: [\"tag:bridge\"]\n    tailnet2:\n      clientId: ${TAILNET2_CLIENT_ID}\n      clientSecret: ${TAILNET2_CLIENT_SECRET}\n      tags: [\"tag:bridge\"]\n\n  directions:\n    \"tailnet1\u003etailnet2\":\n      serviceTags: [\"tag:bridge-svc\"]\n\n  rules:\n    - from: tailnet1\n      to: tailnet2\n      discover:\n        tags: [\"tag:server\"]\n        ports: [22, 80, 443]\n\n  pollInterval: 30s\n  dialTimeout: 10s\n```\n\n```sh\ntailbridge -config config.yaml\n```\n\nMachines tagged `tag:server` on `tailnet1` are now reachable from `tailnet2` on ports 22, 80, 443. Each gets a VIP like `svc:web-1-tailnet1-ts-net`.\n\n## ACL setup\n\nBoth tailnets need:\n\n```jsonc\n{\n  \"tagOwners\": {\n    \"tag:bridge\": [\"autogroup:admin\"],\n    \"tag:bridge-svc\": [\"tag:bridge\"]\n  },\n  \"autoApprovers\": {\n    \"services\": { \"tag:bridge-svc\": [\"tag:bridge\"] }\n  },\n  \"grants\": [\n    {\n      // Clients can reach bridged services\n      \"src\": [\"autogroup:member\", \"autogroup:tagged\"],\n      \"dst\": [\"tag:bridge-svc\", \"tag:bridge\"],\n      \"ip\": [\"*\"]\n    },\n    {\n      // Bridge can reach machines it discovers\n      \"src\": [\"tag:bridge\"],\n      \"dst\": [\"tag:server\"],\n      \"ip\": [\"*\"]\n    }\n  ]\n}\n```\n\nAdjust `dst` in the second grant to match whatever tags your discovered machines use.\n\n## Config reference\n\n| Field | Description |\n|-------|-------------|\n| `bridge.tailnets` | Map of name → OAuth credentials + node tags. Keys must be `[a-z0-9-]+`. |\n| `bridge.directions` | Per-direction settings keyed by `\"from\u003eto\"`. |\n| `directions.*.serviceTags` | Required. ACL tags applied to created VIP services. |\n| `directions.*.prefix` | Optional prefix on VIP service names. |\n| `directions.*.dns.enabled` | Run a DNS server for this direction on TCP port 53. |\n| `directions.*.dns.splitDns` | Auto-configure split-DNS on the destination tailnet. |\n| `directions.*.dns.cleanupOnShutdown` | Remove split-DNS entries on shutdown (default: false). |\n| `bridge.rules` | Discovery rules: `from`, `to`, `discover.tags`, `discover.ports`. |\n| `bridge.pollInterval` | Device poll interval (min 5s, default 30s). |\n| `bridge.dialTimeout` | TCP dial timeout to source machines (default 10s). |\n\n## DNS\n\nWith `dns.enabled: true`, tailbridge runs an authoritative DNS server on the destination tailnet (TCP port 53, via a VIP service). It resolves source machine FQDNs to their bridged VIP IPs.\n\nWith `dns.splitDns: true`, it also configures split-DNS on the destination tailnet so queries for `*.source-tailnet.ts.net` route to the bridge DNS automatically. Clients can then use the original FQDN (e.g. `web-1.tailnet1.ts.net`) and it resolves to the VIP address.\n\n## Bidirectional\n\nAdd directions and rules in both directions:\n\n```yaml\ndirections:\n  \"tailnet1\u003etailnet2\":\n    serviceTags: [\"tag:bridge-svc\"]\n  \"tailnet2\u003etailnet1\":\n    serviceTags: [\"tag:bridge-svc\"]\n\nrules:\n  - from: tailnet1\n    to: tailnet2\n    discover:\n      tags: [\"tag:server\"]\n      ports: [22, 80, 443]\n  - from: tailnet2\n    to: tailnet1\n    discover:\n      tags: [\"tag:iot\"]\n      ports: [80]\n```\n\n## Flags\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `-config` | `config.yaml` | Config file path |\n| `-log-level` | `info` | `debug` / `info` / `warn` / `error` |\n\n## Docker\n\n```sh\ndocker run \\\n  -e TAILNET1_CLIENT_ID=... \\\n  -e TAILNET1_CLIENT_SECRET=... \\\n  -e TAILNET2_CLIENT_ID=... \\\n  -e TAILNET2_CLIENT_SECRET=... \\\n  -v $(pwd)/config.yaml:/config.yaml \\\n  ghcr.io/rajsinghtech/tailbridge:latest \\\n  -config /config.yaml\n```\n\n## How it works\n\nEach poll cycle: Devices API → filter by tags → diff against previous → create/delete VIP services → start/stop TCP listeners → update DNS records → configure split-DNS. Connections are forwarded via `tsnet.Server.Dial()` through the source tailnet's WireGuard tunnel.\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frajsinghtech%2Ftailbridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frajsinghtech%2Ftailbridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frajsinghtech%2Ftailbridge/lists"}