Skip to content

Commit 4b6b0f6

Browse files
committed
Initial commit
0 parents  commit 4b6b0f6

13 files changed

+340
-0
lines changed

.dockerignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/.git
2+
/.github
3+
/.vscode
4+
*.md
5+
.dockerignore
6+
.editorconfig
7+
.env
8+
.gitattributes
9+
.gitignore
10+
.prettierc
11+
Dockerfile
12+
fly.toml
13+
Justfile
14+
package*.json

.editorconfig

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
insert_final_newline = false
6+
7+
[*.md]
8+
indent_size = 4
9+
charset = utf-8
10+
trim_trailing_whitespace = false

.gitattributes

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# NPM files are always saved as LF, even on Windows.
2+
package.json text eol=lf
3+
package-lock.json text eol=lf

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/node_modules
2+
.DS_Store
3+
.env

.prettierrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"overrides": [
3+
{
4+
"files": ["*.md"],
5+
"options": {
6+
"proseWrap": "always"
7+
}
8+
}
9+
]
10+
}

.vscode/settings.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"editor.defaultFormatter": "esbenp.prettier-vscode",
4+
"files.associations": {
5+
"Justfile": "makefile"
6+
}
7+
}

Dockerfile

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
########################################################################
2+
## Ground Control
3+
########################################################################
4+
5+
FROM ghcr.io/malyn/groundcontrol AS groundcontrol
6+
7+
8+
########################################################################
9+
## Tailscale
10+
########################################################################
11+
12+
FROM tailscale/tailscale:v1.38.1 AS tailscale
13+
14+
15+
########################################################################
16+
## Final Image
17+
########################################################################
18+
19+
FROM openresty/openresty:1.21.4.1-6-alpine-apk
20+
21+
# Copy binaries, scripts, and config.
22+
WORKDIR /app
23+
24+
COPY --from=groundcontrol /groundcontrol ./
25+
COPY --from=tailscale /usr/local/bin/tailscaled ./
26+
COPY --from=tailscale /usr/local/bin/tailscale ./
27+
28+
COPY groundcontrol.toml ./
29+
COPY nginx.conf /etc/nginx/conf.d/default.conf
30+
31+
# Run Ground Control to monitor all of the processes.
32+
ENTRYPOINT ["/app/groundcontrol", "/app/groundcontrol.toml"]

Justfile

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Just currently loads the `.env` file, but that will be changing, so we
2+
# are disabling it now since we do not need the functionality.
3+
set dotenv-load := false
4+
5+
_default:
6+
@just --list
7+
8+
# Validate the nginx config.
9+
validate:
10+
@docker build -t dialtun .
11+
@docker run -it --rm \
12+
--entrypoint "/usr/bin/openresty" \
13+
dialtun \
14+
-t
15+
16+
# Run dialtun (including Tailscale).
17+
run:
18+
@docker build -t dialtun .
19+
@docker run -it --rm -p 8080:8080 --env-file .env dialtun

README.md

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# dialtun
2+
3+
**Dynamic routing of public HTTPS endpoints to internal ports on your
4+
Tailscale-connected development machines.**
5+
6+
`dialtun` maps server names in the form `SERVICE-MACHINE.example.com` (where
7+
`example.com` is any domain you control) to internal machines on your Tailnet.
8+
`MACHINE` is the exact name of a machine in the Tailscale network. `SERVICE` is
9+
mapped to a port number by referencing the labels on a (US-based) telephone
10+
dialpad:
11+
12+
<pre style="font-family: Menlo; line-height: 1.2em;">
13+
┌─────────────────────────┐
14+
│ ┌─────┐ ┌─────┐ ┌─────┐ │
15+
│ │ │ │ ABC │ │ DEF │ │
16+
│ │ 1 │ │ 2 │ │ 3 │ │
17+
│ └─────┘ └─────┘ └─────┘ │
18+
│ ┌─────┐ ┌─────┐ ┌─────┐ │
19+
│ │ GHI │ │ JKL │ │ MNO │ │
20+
│ │ 4 │ │ 5 │ │ 6 │ │
21+
│ └─────┘ └─────┘ └─────┘ │
22+
│ ┌─────┐ ┌─────┐ ┌─────┐ │
23+
│ │PQRS │ │ TUV │ │WXYZ │ │
24+
│ │ 7 │ │ 8 │ │ 9 │ │
25+
│ └─────┘ └─────┘ └─────┘ │
26+
│ ┌─────┐ │
27+
│ │ │ │
28+
│ │ 0 │ │
29+
│ └─────┘ │
30+
└─────────────────────────┘
31+
</pre>
32+
33+
A base port number -- 64000, by default -- is added to the first three numbers
34+
of the service name. The final result is that a domain name like
35+
"agendas-devbox1.dev.example.com" would map to port 64243 (64000 + 243) on the
36+
"devbox1" machine on your Tailnet.
37+
38+
## Important Security Warning
39+
40+
`dialtun` does _not_ authenticate incoming connections. Any port in the
41+
configured range (64000-64999, by default) is accessible on the public Internet.
42+
Tailscale ACLs are still consulted, but if you open port 64000 on your machine,
43+
then you should expect that random machines on the Internet _will_ connect to
44+
that port.
45+
46+
In other words, goal of `dialtun` is not to create a _secure_ way to talk to
47+
your internal services -- use normal Tailscale sharing and ACLs for that -- the
48+
goal is agility and flexibility. The _assumption_ is that whatever you are
49+
exposing has its own auth model, or is a simple website that is not secret.
50+
51+
## Setup Instructions
52+
53+
### Create Tailscale ACL Tag
54+
55+
Add an ACL tag to your
56+
[Tailscale Access Controls](https://login.tailscale.com/admin/acls):
57+
58+
1. Add a new tag to the `tagOwners` list, something like this:
59+
60+
```json
61+
// ACL tags.
62+
"tagOwners": {
63+
"tag:dialtun": [],
64+
},
65+
```
66+
67+
2. Add a section for `dialtun` to the `acls` block (assuming you keep the
68+
default port range of 64000-64999):
69+
70+
```json
71+
"acls": [
72+
// ...
73+
74+
// dialtun can access dev server ports on all machines.
75+
{
76+
"action": "accept",
77+
"src": ["tag:dialtun"],
78+
"dst": ["autogroup:members:64000-64999"],
79+
},
80+
81+
// ...
82+
],
83+
```
84+
85+
3. (Optionally) Add an ACL test to verify that `dialtun` can access the dev
86+
server ports, but not any other ports (such as SSH) on the dev machines (this
87+
assumes a dev machine of "devbox1"):
88+
89+
```json
90+
"acls": [
91+
// ...
92+
93+
// dialtun can access the dev ports on all machines (but not SSH).
94+
{
95+
"src": "tag:dialtun",
96+
"accept": ["devbox1:64243", "devbox1:64999"],
97+
"deny": ["devbox1:22"],
98+
},
99+
100+
// ...
101+
],
102+
```
103+
104+
### Create Tailscale Auth Key
105+
106+
Create a Tailscale auth key. The key should be Reusable, Ephemeral, and include
107+
the `dialtun` tag that you defined earlier.
108+
109+
### Create `dialtun` App in Fly.io
110+
111+
_Create_ (but do not yet deploy, since we need to add secrets) the `dialtun` app
112+
on Fly.io:
113+
114+
```shell
115+
$ flyctl launch --build-only --image ghcr.io/malyn/dialtun:latest
116+
```
117+
118+
You can choose an app name, or go with the auto-generated default. The app name
119+
will not be used, assuming that you create the `CNAME` mapping in the next step.
120+
121+
### Create Wildcard TLS Certificate
122+
123+
Add an IP address to your app, then create a _wildcard_ TLS certificate; both
124+
will require adding DNS entries to your DNS server. This is documented on the
125+
Fly.io
126+
[Custom Domains and SSL Certificates page](https://fly.io/docs/app-guides/custom-domains-with-fly/).
127+
128+
### Add Tailscale Auth Key to Fly.io App
129+
130+
Add the Tailscale auth key secret to the app:
131+
132+
```shell
133+
$ flyctl secrets import
134+
TS_AUTHKEY=tskey-auth-...
135+
<Ctrl-D>
136+
```
137+
138+
Using `flyctl secrets import` means that your secrets will not show up in your
139+
shell's command history. Press `Control-D` on a blank line to complete the
140+
input. `flyctl` will respond with "Secrets are staged for the first deployment"
141+
142+
### Deploy the Fly.io App
143+
144+
Deploy the app:
145+
146+
```shell
147+
$ flyctl deploy
148+
```
149+
150+
That's it! You can then access internal machines using `dialtun` URLs.

groundcontrol.toml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[[processes]]
2+
name = "tailscaled"
3+
run = [
4+
"/app/tailscaled",
5+
"--state=/data/tailscale/tailscaled.state",
6+
"--socket=/var/run/tailscale/tailscaled.sock",
7+
"--tun=userspace-networking",
8+
"--outbound-http-proxy-listen=localhost:1055",
9+
]
10+
11+
[[processes]]
12+
name = "tailscale-up"
13+
pre = "/app/tailscale up --authkey={{TS_AUTHKEY}}"
14+
post = "/app/tailscale logout"
15+
16+
[[processes]]
17+
name = "dialtun"
18+
run = [ "/usr/bin/openresty", "-g", "daemon off; env DIALTUN_BASE_PORT;" ]

nginx.conf

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
server {
2+
listen 8080;
3+
listen [::]:8080;
4+
5+
server_name ~^(?<dialtun_service>[A-Za-z]+)-(?<dialtun_host>[A-Za-z0-9\-]+)\.;
6+
7+
location / {
8+
if ($dialtun_service = "") {
9+
return 400;
10+
}
11+
12+
if ($dialtun_host = "") {
13+
return 400;
14+
}
15+
16+
set_by_lua_block $backend {
17+
local DIGITS = {
18+
A=2,B=2,C=2, D=3,E=3,F=3,
19+
G=4,H=4,I=4, J=5,K=5,L=5, M=6,N=6,O=6,
20+
P=7,Q=7,R=7,S=7, T=8,U=8,V=8, W=9,X=9,Y=9,Z=9
21+
}
22+
23+
local letters = string.gsub(string.upper(ngx.var.dialtun_service), "[^A-Z]", "")
24+
local prefix = string.sub(letters, 1, 3)
25+
local numbers = string.gsub(prefix, "[A-Z]", DIGITS)
26+
27+
return ngx.var.dialtun_host .. ':' .. ((os.getenv("DIALTUN_BASE_PORT") or "64000") + numbers);
28+
}
29+
30+
# We need the final URI to be absolute, but if we do the rewrite
31+
# in a single pass then NGINX will see the `http://` prefix and
32+
# generate a (302) redirect. The fix is to build the absolute
33+
# URI in two `redirect` statements so that the NGINX logic will
34+
# not trigger.
35+
rewrite ^(.*)$ "://$backend$1";
36+
rewrite ^(.*)$ "http$1" break;
37+
38+
proxy_pass http://localhost:1055;
39+
}
40+
}

package-lock.json

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "dialtun",
3+
"devDependencies": {
4+
"prettier": "2.8.0"
5+
}
6+
}

0 commit comments

Comments
 (0)