|
|
@ -31,6 +31,7 @@ import (
|
|
|
|
"tailscale.com/net/netutil"
|
|
|
|
"tailscale.com/net/netutil"
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/util/groupmember"
|
|
|
|
"tailscale.com/util/groupmember"
|
|
|
|
|
|
|
|
"tailscale.com/util/httpm"
|
|
|
|
"tailscale.com/version/distro"
|
|
|
|
"tailscale.com/version/distro"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
@ -78,30 +79,6 @@ func init() {
|
|
|
|
template.Must(tmpl.New("web.css").Parse(webCSS))
|
|
|
|
template.Must(tmpl.New("web.css").Parse(webCSS))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type tmplData struct {
|
|
|
|
|
|
|
|
Profile tailcfg.UserProfile
|
|
|
|
|
|
|
|
SynologyUser string
|
|
|
|
|
|
|
|
Status string
|
|
|
|
|
|
|
|
DeviceName string
|
|
|
|
|
|
|
|
IP string
|
|
|
|
|
|
|
|
AdvertiseExitNode bool
|
|
|
|
|
|
|
|
AdvertiseRoutes string
|
|
|
|
|
|
|
|
LicensesURL string
|
|
|
|
|
|
|
|
TUNMode bool
|
|
|
|
|
|
|
|
IsSynology bool
|
|
|
|
|
|
|
|
DSMVersion int // 6 or 7, if IsSynology=true
|
|
|
|
|
|
|
|
IsUnraid bool
|
|
|
|
|
|
|
|
UnraidToken string
|
|
|
|
|
|
|
|
IPNVersion string
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type postedData struct {
|
|
|
|
|
|
|
|
AdvertiseRoutes string
|
|
|
|
|
|
|
|
AdvertiseExitNode bool
|
|
|
|
|
|
|
|
Reauthenticate bool
|
|
|
|
|
|
|
|
ForceLogout bool
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// authorize returns the name of the user accessing the web UI after verifying
|
|
|
|
// authorize returns the name of the user accessing the web UI after verifying
|
|
|
|
// whether the user has access to the web UI. The function will write the
|
|
|
|
// whether the user has access to the web UI. The function will write the
|
|
|
|
// error to the provided http.ResponseWriter.
|
|
|
|
// error to the provided http.ResponseWriter.
|
|
|
@ -294,12 +271,26 @@ req.send(null);
|
|
|
|
// ServeHTTP processes all requests for the Tailscale web client.
|
|
|
|
// ServeHTTP processes all requests for the Tailscale web client.
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if s.devMode {
|
|
|
|
if s.devMode {
|
|
|
|
|
|
|
|
if r.URL.Path == "/api/data" {
|
|
|
|
|
|
|
|
user, err := authorize(w, r)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
switch r.Method {
|
|
|
|
|
|
|
|
case httpm.GET:
|
|
|
|
|
|
|
|
s.serveGetNodeDataJSON(w, r, user)
|
|
|
|
|
|
|
|
case httpm.POST:
|
|
|
|
|
|
|
|
s.servePostNodeUpdate(w, r)
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
// When in dev mode, proxy to the Vite dev server.
|
|
|
|
// When in dev mode, proxy to the Vite dev server.
|
|
|
|
s.devProxy.ServeHTTP(w, r)
|
|
|
|
s.devProxy.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
if authRedirect(w, r) {
|
|
|
|
if authRedirect(w, r) {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -309,25 +300,124 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" {
|
|
|
|
switch {
|
|
|
|
|
|
|
|
case r.URL.Path == "/redirect" || r.URL.Path == "/redirect/":
|
|
|
|
io.WriteString(w, authenticationRedirectHTML)
|
|
|
|
io.WriteString(w, authenticationRedirectHTML)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
case r.Method == "POST":
|
|
|
|
|
|
|
|
s.servePostNodeUpdate(w, r)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
s.serveGetNodeData(w, r, user)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type nodeData struct {
|
|
|
|
|
|
|
|
Profile tailcfg.UserProfile
|
|
|
|
|
|
|
|
SynologyUser string
|
|
|
|
|
|
|
|
Status string
|
|
|
|
|
|
|
|
DeviceName string
|
|
|
|
|
|
|
|
IP string
|
|
|
|
|
|
|
|
AdvertiseExitNode bool
|
|
|
|
|
|
|
|
AdvertiseRoutes string
|
|
|
|
|
|
|
|
LicensesURL string
|
|
|
|
|
|
|
|
TUNMode bool
|
|
|
|
|
|
|
|
IsSynology bool
|
|
|
|
|
|
|
|
DSMVersion int // 6 or 7, if IsSynology=true
|
|
|
|
|
|
|
|
IsUnraid bool
|
|
|
|
|
|
|
|
UnraidToken string
|
|
|
|
|
|
|
|
IPNVersion string
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) getNodeData(ctx context.Context, user string) (*nodeData, error) {
|
|
|
|
st, err := s.lc.Status(ctx)
|
|
|
|
st, err := s.lc.Status(ctx)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
prefs, err := s.lc.GetPrefs(ctx)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
profile := st.User[st.Self.UserID]
|
|
|
|
|
|
|
|
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
|
|
|
|
|
|
|
versionShort := strings.Split(st.Version, "-")[0]
|
|
|
|
|
|
|
|
data := &nodeData{
|
|
|
|
|
|
|
|
SynologyUser: user,
|
|
|
|
|
|
|
|
Profile: profile,
|
|
|
|
|
|
|
|
Status: st.BackendState,
|
|
|
|
|
|
|
|
DeviceName: deviceName,
|
|
|
|
|
|
|
|
LicensesURL: licenses.LicensesURL(),
|
|
|
|
|
|
|
|
TUNMode: st.TUN,
|
|
|
|
|
|
|
|
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
|
|
|
|
|
|
|
|
DSMVersion: distro.DSMVersion(),
|
|
|
|
|
|
|
|
IsUnraid: distro.Get() == distro.Unraid,
|
|
|
|
|
|
|
|
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
|
|
|
|
|
|
|
|
IPNVersion: versionShort,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
|
|
|
|
|
|
|
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
|
|
|
|
|
|
|
for _, r := range prefs.AdvertiseRoutes {
|
|
|
|
|
|
|
|
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
|
|
|
|
|
|
|
data.AdvertiseExitNode = true
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if data.AdvertiseRoutes != "" {
|
|
|
|
|
|
|
|
data.AdvertiseRoutes += ","
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
data.AdvertiseRoutes += r.String()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(st.TailscaleIPs) != 0 {
|
|
|
|
|
|
|
|
data.IP = st.TailscaleIPs[0].String()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request, user string) {
|
|
|
|
|
|
|
|
data, err := s.getNodeData(r.Context(), user)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prefs, err := s.lc.GetPrefs(ctx)
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
|
|
|
if err := tmpl.Execute(buf, *data); err != nil {
|
|
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Write(buf.Bytes())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request, user string) {
|
|
|
|
|
|
|
|
data, err := s.getNodeData(r.Context(), user)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewEncoder(w).Encode(*data); err != nil {
|
|
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type nodeUpdate struct {
|
|
|
|
|
|
|
|
AdvertiseRoutes string
|
|
|
|
|
|
|
|
AdvertiseExitNode bool
|
|
|
|
|
|
|
|
Reauthenticate bool
|
|
|
|
|
|
|
|
ForceLogout bool
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if r.Method == "POST" {
|
|
|
|
func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
|
|
|
|
defer r.Body.Close()
|
|
|
|
defer r.Body.Close()
|
|
|
|
var postData postedData
|
|
|
|
|
|
|
|
|
|
|
|
st, err := s.lc.Status(r.Context())
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var postData nodeUpdate
|
|
|
|
type mi map[string]any
|
|
|
|
type mi map[string]any
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
|
|
|
|
w.WriteHeader(400)
|
|
|
|
w.WriteHeader(400)
|
|
|
@ -349,7 +439,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
mp.Prefs.AdvertiseRoutes = routes
|
|
|
|
mp.Prefs.AdvertiseRoutes = routes
|
|
|
|
log.Printf("Doing edit: %v", mp.Pretty())
|
|
|
|
log.Printf("Doing edit: %v", mp.Pretty())
|
|
|
|
|
|
|
|
|
|
|
|
if _, err := s.lc.EditPrefs(ctx, mp); err != nil {
|
|
|
|
if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
|
|
|
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
|
|
|
return
|
|
|
|
return
|
|
|
@ -377,49 +467,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
io.WriteString(w, "{}")
|
|
|
|
io.WriteString(w, "{}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
profile := st.User[st.Self.UserID]
|
|
|
|
|
|
|
|
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
|
|
|
|
|
|
|
versionShort := strings.Split(st.Version, "-")[0]
|
|
|
|
|
|
|
|
data := tmplData{
|
|
|
|
|
|
|
|
SynologyUser: user,
|
|
|
|
|
|
|
|
Profile: profile,
|
|
|
|
|
|
|
|
Status: st.BackendState,
|
|
|
|
|
|
|
|
DeviceName: deviceName,
|
|
|
|
|
|
|
|
LicensesURL: licenses.LicensesURL(),
|
|
|
|
|
|
|
|
TUNMode: st.TUN,
|
|
|
|
|
|
|
|
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
|
|
|
|
|
|
|
|
DSMVersion: distro.DSMVersion(),
|
|
|
|
|
|
|
|
IsUnraid: distro.Get() == distro.Unraid,
|
|
|
|
|
|
|
|
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
|
|
|
|
|
|
|
|
IPNVersion: versionShort,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
|
|
|
|
|
|
|
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
|
|
|
|
|
|
|
for _, r := range prefs.AdvertiseRoutes {
|
|
|
|
|
|
|
|
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
|
|
|
|
|
|
|
data.AdvertiseExitNode = true
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if data.AdvertiseRoutes != "" {
|
|
|
|
|
|
|
|
data.AdvertiseRoutes += ","
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
data.AdvertiseRoutes += r.String()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(st.TailscaleIPs) != 0 {
|
|
|
|
|
|
|
|
data.IP = st.TailscaleIPs[0].String()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
|
|
|
if err := tmpl.Execute(buf, data); err != nil {
|
|
|
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Write(buf.Bytes())
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData postedData) (authURL string, retErr error) {
|
|
|
|
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) {
|
|
|
|
if postData.ForceLogout {
|
|
|
|
if postData.ForceLogout {
|
|
|
|
if err := s.lc.Logout(ctx); err != nil {
|
|
|
|
if err := s.lc.Logout(ctx); err != nil {
|
|
|
|
return "", fmt.Errorf("Logout error: %w", err)
|
|
|
|
return "", fmt.Errorf("Logout error: %w", err)
|
|
|
|