Home

tailroute @main - refs - log -
-
https://git.jolheiser.com/tailroute.git
Router for tailnet and funnel across tailscale
tailroute / tailroute.go
- raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package tailroute

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	"tailscale.com/ipn"
	"tailscale.com/tsnet"
	"tailscale.com/types/logger"
)

type handlerCtxKey int

const (
	privacyKey handlerCtxKey = iota
	isFunnel
	isTailnet
)

type Router struct {
	Funnel  http.Handler
	Tailnet http.Handler
	Logger  logger.Logf
}

func (router Router) serve(ln net.Listener) error {
	srv := &http.Server{
		ConnContext: func(ctx context.Context, c net.Conn) context.Context {
			tc, ok := c.(*tls.Conn)
			if !ok {
				return ctx
			}
			if _, ok := tc.NetConn().(*ipn.FunnelConn); ok {
				return context.WithValue(ctx, privacyKey, isFunnel)
			} else {
				return context.WithValue(ctx, privacyKey, isTailnet)
			}
		},
		Handler: http.HandlerFunc(router.serveHTTP),
	}

	return srv.Serve(ln)
}

func (router Router) serveHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	valAny := ctx.Value(privacyKey)
	if valAny == nil {
		panic("incorrect context stack (value is missing)")
	}

	val, ok := valAny.(handlerCtxKey)
	if !ok {
		panic("incorrect context stack (value is of wrong type)")
	}

	switch val {
	case isFunnel:
		router.Funnel.ServeHTTP(w, r)
		return
	case isTailnet:
		router.Tailnet.ServeHTTP(w, r)
		return
	}

	panic("unknown security level")
}

func (router Router) Serve(hostname string, dataDir string) error {
	if router.Tailnet == nil {
		if router.Funnel == nil {
			return errors.New("tailroute requires at least one router")
		}
		router.Tailnet = router.Funnel
	}
	if err := os.MkdirAll(dataDir, os.ModePerm); err != nil {
		return fmt.Errorf("could not create data dir: %w", err)
	}
	s := &tsnet.Server{
		Hostname: hostname,
		Dir:      dataDir,
		Logf:     router.Logger,
	}
	if err := s.Start(); err != nil {
		return err
	}

	lc, err := s.LocalClient()
	if err != nil {
		return err
	}

	for {
		upCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		status, err := s.Up(upCtx)
		if err == nil && status != nil {
			break
		}
	}

	ctx := context.Background()
	_, ok := lc.ExpandSNIName(ctx, hostname)
	if !ok {
		return errors.New("HTTPS is not enabled in the admin panel")
	}

	lnHttp, err := s.Listen("tcp", ":80")
	if err != nil {
		return fmt.Errorf("can't listen http: %w", err)
	}
	defer lnHttp.Close()
	go func() {
		http.Serve(lnHttp, router.Tailnet)
	}()

	var lnHttps net.Listener
	if router.Funnel == nil {
		lnHttps, err = s.ListenTLS("tcp", ":443")
	} else {
		lnHttps, err = s.ListenFunnel("tcp", ":443")
	}
	if err != nil {
		return fmt.Errorf("can't listen https: %w", err)
	}
	defer lnHttps.Close()

	return router.serve(lnHttps)
}