mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-07 16:54:37 +00:00

We are soon going to start assigning shared-in nodes a CGNAT IPv4 in the Hello tailnet when necessary, the same way that normal node shares assign a new IPv4 on conflict. But Hello wants to display the node's native IPv4, the one it uses in its own tailnet. That IPv4 isn't available anywhere in the netmap today, because it's not normally needed for anything. We are going to start sending that native IPv4 in the peer node CapMap, only for Hello's netmap responses. This change enables Hello to display that native IPv4 instead, when available. Updates tailscale/corp#25393 Change-Id: I87480b6d318ab028b41ef149eb3ba618bd7f1e08 Signed-off-by: Brian Palmer <brianp@tailscale.com>
217 lines
5.6 KiB
Go
217 lines
5.6 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// The hello binary runs hello.ts.net.
|
|
package main // import "tailscale.com/cmd/hello"
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"tailscale.com/client/local"
|
|
"tailscale.com/client/tailscale/apitype"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
var (
|
|
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
|
|
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
|
|
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
|
|
)
|
|
|
|
//go:embed hello.tmpl.html
|
|
var embeddedTemplate string
|
|
|
|
var localClient local.Client
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
if *testIP != "" {
|
|
res, err := localClient.WhoIs(context.Background(), *testIP)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
e := json.NewEncoder(os.Stdout)
|
|
e.SetIndent("", "\t")
|
|
e.Encode(res)
|
|
return
|
|
}
|
|
if devMode() {
|
|
// Parse it optimistically
|
|
var err error
|
|
tmpl, err = template.New("home").Parse(embeddedTemplate)
|
|
if err != nil {
|
|
log.Printf("ignoring template error in dev mode: %v", err)
|
|
}
|
|
} else {
|
|
if embeddedTemplate == "" {
|
|
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
|
}
|
|
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
|
|
}
|
|
|
|
http.HandleFunc("/", root)
|
|
log.Printf("Starting hello server.")
|
|
|
|
errc := make(chan error, 1)
|
|
if *httpAddr != "" {
|
|
log.Printf("running HTTP server on %s", *httpAddr)
|
|
go func() {
|
|
errc <- http.ListenAndServe(*httpAddr, nil)
|
|
}()
|
|
}
|
|
if *httpsAddr != "" {
|
|
log.Printf("running HTTPS server on %s", *httpsAddr)
|
|
go func() {
|
|
hs := &http.Server{
|
|
Addr: *httpsAddr,
|
|
TLSConfig: &tls.Config{
|
|
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
switch hi.ServerName {
|
|
case "hello.ts.net":
|
|
return localClient.GetCertificate(hi)
|
|
case "hello.ipn.dev":
|
|
c, err := tls.LoadX509KeyPair(
|
|
"/etc/hello/hello.ipn.dev.crt",
|
|
"/etc/hello/hello.ipn.dev.key",
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
return nil, errors.New("invalid SNI name")
|
|
},
|
|
},
|
|
IdleTimeout: 30 * time.Second,
|
|
ReadHeaderTimeout: 20 * time.Second,
|
|
MaxHeaderBytes: 10 << 10,
|
|
}
|
|
errc <- hs.ListenAndServeTLS("", "")
|
|
}()
|
|
}
|
|
log.Fatal(<-errc)
|
|
}
|
|
|
|
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
|
|
|
func getTmpl() (*template.Template, error) {
|
|
if devMode() {
|
|
tmplData, err := os.ReadFile("hello.tmpl.html")
|
|
if os.IsNotExist(err) {
|
|
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
|
|
return tmpl, nil
|
|
}
|
|
return template.New("home").Parse(string(tmplData))
|
|
}
|
|
return tmpl, nil
|
|
}
|
|
|
|
// tmpl is the template used in prod mode.
|
|
// In dev mode it's only used if the template file doesn't exist on disk.
|
|
// It's initialized by main after flag parsing.
|
|
var tmpl *template.Template
|
|
|
|
type tmplData struct {
|
|
DisplayName string // "Foo Barberson"
|
|
LoginName string // "foo@bar.com"
|
|
ProfilePicURL string // "https://..."
|
|
MachineName string // "imac5k"
|
|
MachineOS string // "Linux"
|
|
IP string // "100.2.3.4"
|
|
}
|
|
|
|
func tailscaleIP(who *apitype.WhoIsResponse) string {
|
|
if who == nil {
|
|
return ""
|
|
}
|
|
vals, err := tailcfg.UnmarshalNodeCapJSON[string](who.Node.CapMap, tailcfg.NodeAttrNativeIPV4)
|
|
if err == nil && len(vals) > 0 {
|
|
return vals[0]
|
|
}
|
|
for _, nodeIP := range who.Node.Addresses {
|
|
if nodeIP.Addr().Is4() && nodeIP.IsSingleIP() {
|
|
return nodeIP.Addr().String()
|
|
}
|
|
}
|
|
for _, nodeIP := range who.Node.Addresses {
|
|
if nodeIP.IsSingleIP() {
|
|
return nodeIP.Addr().String()
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func root(w http.ResponseWriter, r *http.Request) {
|
|
if r.TLS == nil && *httpsAddr != "" {
|
|
host := r.Host
|
|
if strings.Contains(r.Host, "100.101.102.103") ||
|
|
strings.Contains(r.Host, "hello.ipn.dev") {
|
|
host = "hello.ts.net"
|
|
}
|
|
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
|
return
|
|
}
|
|
if r.RequestURI != "/" {
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
return
|
|
}
|
|
if r.TLS != nil && *httpsAddr != "" && strings.Contains(r.Host, "hello.ipn.dev") {
|
|
http.Redirect(w, r, "https://hello.ts.net", http.StatusFound)
|
|
return
|
|
}
|
|
tmpl, err := getTmpl()
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
http.Error(w, "template error: "+err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
who, err := localClient.WhoIs(r.Context(), r.RemoteAddr)
|
|
var data tmplData
|
|
if err != nil {
|
|
if devMode() {
|
|
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
|
|
data = tmplData{
|
|
DisplayName: "Taily Scalerson",
|
|
LoginName: "taily@scaler.son",
|
|
ProfilePicURL: "https://placekitten.com/200/200",
|
|
MachineName: "scaled",
|
|
MachineOS: "Linux",
|
|
IP: "100.1.2.3",
|
|
}
|
|
} else {
|
|
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
|
|
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
|
return
|
|
}
|
|
} else {
|
|
data = tmplData{
|
|
DisplayName: who.UserProfile.DisplayName,
|
|
LoginName: who.UserProfile.LoginName,
|
|
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
|
MachineName: firstLabel(who.Node.ComputedName),
|
|
MachineOS: who.Node.Hostinfo.OS(),
|
|
IP: tailscaleIP(who),
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
tmpl.Execute(w, data)
|
|
}
|
|
|
|
// firstLabel s up until the first period, if any.
|
|
func firstLabel(s string) string {
|
|
s, _, _ = strings.Cut(s, ".")
|
|
return s
|
|
}
|