tangled
alpha
login
or
join now
sr.aux1.dev
/
tsproxy
2
fork
atom
HTTP reverse proxy for Tailscale
2
fork
atom
overview
issues
pulls
1
pipelines
redirect https to fqdn
sr.aux1.dev
11 months ago
333695d9
995cdd32
+89
-2
2 changed files
expand all
collapse all
unified
split
main.go
tsproxy_test.go
+29
-1
main.go
···
16
16
"path/filepath"
17
17
"sort"
18
18
"strconv"
19
19
+
"strings"
19
20
"syscall"
20
21
21
22
"github.com/oklog/run"
···
27
28
"github.com/tailscale/hujson"
28
29
"tailscale.com/client/local"
29
30
"tailscale.com/client/tailscale/apitype"
31
31
+
"tailscale.com/ipn/ipnstate"
30
32
"tailscale.com/tsnet"
31
33
tslogger "tailscale.com/types/logger"
32
34
)
···
356
358
357
359
// localTailnetTLSHandler serves HTTPS on the local tailnet.
358
360
func localTailnetTLSHandler(logger *slog.Logger, lc tailscaleLocalClient, next http.Handler) http.Handler {
359
359
-
return localTailnetHandler(logger, lc, next)
361
361
+
var (
362
362
+
handler = localTailnetHandler(logger, lc, next)
363
363
+
dnsName string
364
364
+
)
365
365
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
366
366
+
if r.TLS == nil {
367
367
+
panic("TLS handler wants TLS")
368
368
+
}
369
369
+
370
370
+
if dnsName == "" {
371
371
+
st, err := lc.StatusWithoutPeers(r.Context())
372
372
+
if err != nil {
373
373
+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
374
374
+
logger.Error("tailscale status", slog.Any("err", err))
375
375
+
return
376
376
+
}
377
377
+
dnsName = strings.TrimSuffix(st.Self.DNSName, ".")
378
378
+
}
379
379
+
380
380
+
if strings.TrimSuffix(r.Host, ".") != dnsName {
381
381
+
http.Redirect(w, r, fmt.Sprintf("https://%s%s", dnsName, r.RequestURI), http.StatusPermanentRedirect)
382
382
+
return
383
383
+
}
384
384
+
385
385
+
handler.ServeHTTP(w, r)
386
386
+
})
360
387
}
361
388
362
389
// insecureFunnelHandler handles HTTPS requests coming from Tailscale Funnel nodes.
···
368
395
369
396
type tailscaleLocalClient interface {
370
397
WhoIs(context.Context, string) (*apitype.WhoIsResponse, error)
398
398
+
StatusWithoutPeers(context.Context) (*ipnstate.Status, error)
371
399
}
372
400
373
401
func tsWhoIs(lc tailscaleLocalClient, r *http.Request) (*apitype.WhoIsResponse, error) {
+60
-1
tsproxy_test.go
···
14
14
"github.com/prometheus/client_golang/prometheus"
15
15
"github.com/prometheus/client_golang/prometheus/testutil"
16
16
"tailscale.com/client/tailscale/apitype"
17
17
+
"tailscale.com/ipn/ipnstate"
17
18
"tailscale.com/tailcfg"
18
19
)
19
20
20
21
type fakeLocalClient struct {
21
21
-
whois func(context.Context, string) (*apitype.WhoIsResponse, error)
22
22
+
whois func(context.Context, string) (*apitype.WhoIsResponse, error)
23
23
+
status func(context.Context) (*ipnstate.Status, error)
22
24
}
23
25
24
26
func (c *fakeLocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
25
27
return c.whois(ctx, remoteAddr)
28
28
+
}
29
29
+
30
30
+
func (c *fakeLocalClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
31
31
+
return c.status(ctx)
26
32
}
27
33
28
34
func TestLocalTailnetHandler(t *testing.T) {
···
108
114
}
109
115
}
110
116
})
117
117
+
}
118
118
+
}
119
119
+
120
120
+
func TestLocalTailnetTLSHandler(t *testing.T) {
121
121
+
t.Parallel()
122
122
+
123
123
+
lc := &fakeLocalClient{
124
124
+
whois: func(_ context.Context, _ string) (*apitype.WhoIsResponse, error) {
125
125
+
return &apitype.WhoIsResponse{UserProfile: &tailcfg.UserProfile{LoginName: "tagged-devices"}, Node: &tailcfg.Node{Tags: []string{"foo"}}}, nil
126
126
+
},
127
127
+
status: func(_ context.Context) (*ipnstate.Status, error) {
128
128
+
return &ipnstate.Status{Self: &ipnstate.PeerStatus{DNSName: "foo.ts.net."}}, nil
129
129
+
},
130
130
+
}
131
131
+
be := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132
132
+
fmt.Fprintln(w, "Hi from the backend.")
133
133
+
})
134
134
+
px := httptest.NewTLSServer(localTailnetTLSHandler(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})), lc, be))
135
135
+
defer px.Close()
136
136
+
137
137
+
cli := px.Client()
138
138
+
cli.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
139
139
+
return http.ErrUseLastResponse
140
140
+
}
141
141
+
142
142
+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, px.URL+"/bar", nil)
143
143
+
if err != nil {
144
144
+
t.Fatal(err)
145
145
+
}
146
146
+
resp, err := cli.Do(req)
147
147
+
if err != nil {
148
148
+
t.Fatal(err)
149
149
+
}
150
150
+
defer resp.Body.Close()
151
151
+
if want, got := http.StatusPermanentRedirect, resp.StatusCode; want != got {
152
152
+
t.Fatalf("want status %d, got: %d", want, got)
153
153
+
}
154
154
+
if want, got := "https://foo.ts.net/bar", resp.Header.Get("location"); got != want {
155
155
+
t.Fatalf("want Location %s, got: %s", want, got)
156
156
+
}
157
157
+
158
158
+
req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, px.URL, nil)
159
159
+
if err != nil {
160
160
+
t.Fatal(err)
161
161
+
}
162
162
+
req.Host = "foo.ts.net"
163
163
+
resp, err = px.Client().Do(req)
164
164
+
if err != nil {
165
165
+
t.Fatal(err)
166
166
+
}
167
167
+
defer resp.Body.Close()
168
168
+
if want, got := http.StatusOK, resp.StatusCode; want != got {
169
169
+
t.Fatalf("want status %d, got: %d", want, got)
111
170
}
112
171
}
113
172