├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum └── tsid.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | commit-message: 8 | prefix: '.github:' 9 | - package-ecosystem: 'gomod' 10 | directory: '/' 11 | schedule: 12 | interval: 'weekly' 13 | commit-message: 14 | prefix: 'go.mod:' 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [master] 5 | paths-ignore: 6 | - '*.md' 7 | pull_request: 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out 14 | uses: actions/checkout@v3 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: 1.19 19 | - name: Cache dependencies and build cache 20 | uses: actions/cache@v3 21 | with: 22 | path: | 23 | ~/go/pkg/mod 24 | ~/.cache/go-build 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | - name: Install xcaddy 27 | run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 28 | - name: Build 29 | run: xcaddy build --with go.astrophena.name/tsid=. 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | © 2021 Ilya Mateyko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `tsid` is no longer maintained. Feel free to fork. 2 | 3 | --- 4 | 5 | `tsid` is a [Caddy] plugin that allows access only to requests 6 | coming from the [Tailscale] network and allows to identify users 7 | behind these requests by setting some [Caddy] [placeholders]: 8 | 9 | | Placeholder | Description | 10 | |------------------------------|-------------| 11 | | `{http.vars.tailscale.name}` | User name | 12 | | `{http.vars.tailscale.email}`| User email | 13 | 14 | ## Usage 15 | 16 | 1. Build Caddy with this plugin by [xcaddy]: 17 | 18 | $ xcaddy build --with go.astrophena.name/tsid 19 | 20 | 2. Make sure that `tsid` is ordered first: 21 | 22 | { 23 | order tsid first 24 | } 25 | 26 | 3. Add the `tsid` directive to your Caddyfile and use the placeholders: 27 | 28 | tsid 29 | 30 | respond "Hello, {http.vars.tailscale.name}!" 31 | 32 | ## License 33 | 34 | [MIT] © Ilya Mateyko 35 | 36 | [Caddy]: https://caddyserver.com 37 | [Tailscale]: https://tailscale.com 38 | [placeholders]: https://caddyserver.com/docs/conventions#placeholders 39 | [xcaddy]: https://github.com/caddyserver/xcaddy 40 | [MIT]: LICENSE.md 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.astrophena.name/tsid 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.38.52 // indirect 7 | github.com/caddyserver/caddy/v2 v2.5.1 8 | inet.af/netaddr v0.0.0-20220617031823-097006376321 9 | tailscale.com v1.30.0 10 | ) 11 | -------------------------------------------------------------------------------- /tsid.go: -------------------------------------------------------------------------------- 1 | // © 2021 Ilya Mateyko. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE.md file. 4 | 5 | // Package tsid is a Caddy plugin that allows access only to 6 | // requests coming from the Tailscale network and allows to identify 7 | // users behind these requests by setting some Caddy placeholders. 8 | package tsid 9 | 10 | import ( 11 | "errors" 12 | "net" 13 | "net/http" 14 | "strings" 15 | 16 | "github.com/caddyserver/caddy/v2" 17 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 18 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 19 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 20 | "inet.af/netaddr" 21 | "tailscale.com/client/tailscale" 22 | "tailscale.com/net/tsaddr" 23 | ) 24 | 25 | func init() { 26 | caddy.RegisterModule(&Middleware{}) 27 | httpcaddyfile.RegisterHandlerDirective("tsid", parseCaddyfileHandler) 28 | } 29 | 30 | // CaddyModule returns the Caddy module information. 31 | func (Middleware) CaddyModule() caddy.ModuleInfo { 32 | return caddy.ModuleInfo{ 33 | ID: "http.handlers.tsid", 34 | New: func() caddy.Module { return &Middleware{} }, 35 | } 36 | } 37 | 38 | // Middleware is a Caddy HTTP handler that allows requests only from 39 | // the Tailscale network and sets placeholders based on the Tailscale 40 | // node information. 41 | type Middleware struct{} 42 | 43 | // ServeHTTP implements the caddyhttp.MiddlewareHandler interface. 44 | func (Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { 45 | ipStr, _, err := net.SplitHostPort(r.RemoteAddr) 46 | if err != nil { 47 | return caddyhttp.Error(http.StatusInternalServerError, err) 48 | } 49 | 50 | ip, err := netaddr.ParseIP(ipStr) 51 | if err != nil { 52 | return caddyhttp.Error(http.StatusInternalServerError, err) 53 | } 54 | 55 | if !tsaddr.IsTailscaleIP(ip) { 56 | return caddyhttp.Error(http.StatusForbidden, errors.New("not a Tailscale IP")) 57 | } 58 | 59 | whois, err := tailscale.WhoIs(r.Context(), r.RemoteAddr) 60 | if err != nil { 61 | if strings.Contains(err.Error(), "no match for IP:port") { 62 | return caddyhttp.Error(http.StatusForbidden, errors.New("not authorized")) 63 | } 64 | return caddyhttp.Error(http.StatusInternalServerError, err) 65 | } 66 | 67 | caddyhttp.SetVar(r.Context(), "tailscale.name", whois.UserProfile.DisplayName) 68 | caddyhttp.SetVar(r.Context(), "tailscale.email", whois.UserProfile.LoginName) 69 | 70 | return next.ServeHTTP(w, r) 71 | } 72 | 73 | // UnmarshalCaddyfile implements the caddyfile.Unmarshaler interface. 74 | func (Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } 75 | 76 | // parseCaddyfileHandler unmarshals tokens from h into a new middleware handler value. 77 | func parseCaddyfileHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 78 | m := &Middleware{} 79 | err := m.UnmarshalCaddyfile(h.Dispenser) 80 | return m, err 81 | } 82 | 83 | // Interface guards. 84 | var ( 85 | _ caddyhttp.MiddlewareHandler = (*Middleware)(nil) 86 | _ caddyfile.Unmarshaler = (*Middleware)(nil) 87 | ) 88 | --------------------------------------------------------------------------------