mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
cli: web advertise exit node button
A couple of gnarly assumptions in this code, as always with the async message thing. UI button is based on the DNS settings in the admin panel. Co-authored-by: Maisem Ali <maisem@tailscale.com> Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
This commit is contained in:
parent
0410f1a35a
commit
cc9cf97cbe
@ -143,17 +143,11 @@ func warnf(format string, args ...interface{}) {
|
|||||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||||
)
|
)
|
||||||
|
|
||||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
|
||||||
//
|
|
||||||
// Note that the parameters upArgs and warnf are named intentionally
|
|
||||||
// to shadow the globals to prevent accidental misuse of them. This
|
|
||||||
// function exists for testing and should have no side effects or
|
|
||||||
// outside interactions (e.g. no making Tailscale local API calls).
|
|
||||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
|
||||||
routeMap := map[netaddr.IPPrefix]bool{}
|
routeMap := map[netaddr.IPPrefix]bool{}
|
||||||
var default4, default6 bool
|
if advertiseRoutes != "" {
|
||||||
if upArgs.advertiseRoutes != "" {
|
var default4, default6 bool
|
||||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
advroutes := strings.Split(advertiseRoutes, ",")
|
||||||
for _, s := range advroutes {
|
for _, s := range advroutes {
|
||||||
ipp, err := netaddr.ParseIPPrefix(s)
|
ipp, err := netaddr.ParseIPPrefix(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -175,7 +169,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
|||||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if upArgs.advertiseDefaultRoute {
|
if advertiseDefaultRoute {
|
||||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||||
}
|
}
|
||||||
@ -189,6 +183,20 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
|||||||
}
|
}
|
||||||
return routes[i].IP().Less(routes[j].IP())
|
return routes[i].IP().Less(routes[j].IP())
|
||||||
})
|
})
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||||
|
//
|
||||||
|
// Note that the parameters upArgs and warnf are named intentionally
|
||||||
|
// to shadow the globals to prevent accidental misuse of them. This
|
||||||
|
// function exists for testing and should have no side effects or
|
||||||
|
// outside interactions (e.g. no making Tailscale local API calls).
|
||||||
|
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||||
|
routes, err := calcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var exitNodeIP netaddr.IP
|
var exitNodeIP netaddr.IP
|
||||||
if upArgs.exitNodeIP != "" {
|
if upArgs.exitNodeIP != "" {
|
||||||
|
@ -1335,3 +1335,14 @@ html {
|
|||||||
border-color: #6c94ec;
|
border-color: #6c94ec;
|
||||||
border-color: rgba(108, 148, 236, var(--border-opacity));
|
border-color: rgba(108, 148, 236, var(--border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-red {
|
||||||
|
background-color: #d04841;
|
||||||
|
border-color: #d04841;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-red:enabled:hover {
|
||||||
|
background-color: #b22d30;
|
||||||
|
border-color: #b22d30;
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@ -26,6 +27,7 @@
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"inet.af/netaddr"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@ -51,11 +53,13 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type tmplData struct {
|
type tmplData struct {
|
||||||
Profile tailcfg.UserProfile
|
Profile tailcfg.UserProfile
|
||||||
SynologyUser string
|
SynologyUser string
|
||||||
Status string
|
Status string
|
||||||
DeviceName string
|
DeviceName string
|
||||||
IP string
|
IP string
|
||||||
|
AdvertiseExitNode bool
|
||||||
|
AdvertiseRoutes string
|
||||||
}
|
}
|
||||||
|
|
||||||
var webCmd = &ffcli.Command{
|
var webCmd = &ffcli.Command{
|
||||||
@ -303,15 +307,45 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
|
defer r.Body.Close()
|
||||||
|
var postData struct {
|
||||||
|
AdvertiseRoutes string
|
||||||
|
AdvertiseExitNode bool
|
||||||
|
Reauthenticate bool
|
||||||
|
}
|
||||||
type mi map[string]interface{}
|
type mi map[string]interface{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs, err := tailscale.GetPrefs(r.Context())
|
||||||
|
if err != nil && !postData.Reauthenticate {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefs.AdvertiseRoutes = routes
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
url, err := tailscaleUpForceReauth(r.Context())
|
url, err := tailscaleUp(r.Context(), prefs, postData.Reauthenticate)
|
||||||
if err != nil {
|
if 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
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(mi{"url": url})
|
if url != "" {
|
||||||
|
json.NewEncoder(w).Encode(mi{"url": url})
|
||||||
|
} else {
|
||||||
|
io.WriteString(w, "{}")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,6 +354,11 @@ type mi map[string]interface{
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
prefs, err := tailscale.GetPrefs(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
profile := st.User[st.Self.UserID]
|
profile := st.User[st.Self.UserID]
|
||||||
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
||||||
@ -329,6 +368,18 @@ type mi map[string]interface{
|
|||||||
Status: st.BackendState,
|
Status: st.BackendState,
|
||||||
DeviceName: deviceName,
|
DeviceName: deviceName,
|
||||||
}
|
}
|
||||||
|
exitNodeRouteV4 := netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||||
|
exitNodeRouteV6 := netaddr.MustParseIPPrefix("::/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 {
|
if len(st.TailscaleIPs) != 0 {
|
||||||
data.IP = st.TailscaleIPs[0].String()
|
data.IP = st.TailscaleIPs[0].String()
|
||||||
}
|
}
|
||||||
@ -342,13 +393,15 @@ type mi map[string]interface{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||||
func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) {
|
func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authURL string, retErr error) {
|
||||||
prefs := ipn.NewPrefs()
|
if prefs == nil {
|
||||||
prefs.ControlURL = ipn.DefaultControlURL
|
prefs = ipn.NewPrefs()
|
||||||
prefs.WantRunning = true
|
prefs.ControlURL = ipn.DefaultControlURL
|
||||||
prefs.CorpDNS = true
|
prefs.WantRunning = true
|
||||||
prefs.AllowSingleHosts = true
|
prefs.CorpDNS = true
|
||||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
prefs.AllowSingleHosts = true
|
||||||
|
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||||
|
}
|
||||||
|
|
||||||
if distro.Get() == distro.Synology {
|
if distro.Get() == distro.Synology {
|
||||||
prefs.NetfilterMode = preftype.NetfilterOff
|
prefs.NetfilterMode = preftype.NetfilterOff
|
||||||
@ -395,6 +448,14 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
|||||||
authURL = *url
|
authURL = *url
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
if !forceReauth && n.Prefs != nil {
|
||||||
|
p1, p2 := *n.Prefs, *prefs
|
||||||
|
p1.Persist = nil
|
||||||
|
p2.Persist = nil
|
||||||
|
if p1.Equals(&p2) {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// Wait for backend client to be connected so we know
|
// Wait for backend client to be connected so we know
|
||||||
// we're subscribed to updates. Otherwise we can miss
|
// we're subscribed to updates. Otherwise we can miss
|
||||||
@ -412,10 +473,15 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
|||||||
bc.Start(ipn.Options{
|
bc.Start(ipn.Options{
|
||||||
StateKey: ipn.GlobalDaemonStateKey,
|
StateKey: ipn.GlobalDaemonStateKey,
|
||||||
})
|
})
|
||||||
bc.StartLoginInteractive()
|
if forceReauth {
|
||||||
|
bc.StartLoginInteractive()
|
||||||
|
}
|
||||||
|
|
||||||
<-pumpCtx.Done() // wait for authURL or complete failure
|
<-pumpCtx.Done() // wait for authURL or complete failure
|
||||||
if authURL == "" && retErr == nil {
|
if authURL == "" && retErr == nil {
|
||||||
|
if !forceReauth {
|
||||||
|
return "", nil // no auth URL is fine
|
||||||
|
}
|
||||||
retErr = pumpCtx.Err()
|
retErr = pumpCtx.Err()
|
||||||
}
|
}
|
||||||
if authURL == "" && retErr == nil {
|
if authURL == "" && retErr == nil {
|
||||||
|
@ -86,14 +86,30 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
<div class="mb-4">
|
||||||
|
<a href="#" class="mb-4 js-advertiseExitNode">
|
||||||
|
{{if .AdvertiseExitNode}}
|
||||||
|
<button class="button button-red button-medium" id="enabled">Stop advertising Exit Node</button>
|
||||||
|
{{else}}
|
||||||
|
<button class="button button-blue button-medium" id="enabled">Advertise as Exit Node</button>
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</main>
|
</main>
|
||||||
<script>(function () {
|
<script>(function () {
|
||||||
let loginButtons = document.querySelectorAll(".js-loginButton");
|
const advertiseExitNode = {{.AdvertiseExitNode}};
|
||||||
let fetchingUrl = false;
|
let fetchingUrl = false;
|
||||||
|
var data = {
|
||||||
|
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
|
||||||
|
AdvertiseExitNode: advertiseExitNode,
|
||||||
|
Reauthenticate: false
|
||||||
|
};
|
||||||
|
|
||||||
function handleClick(e) {
|
function postData(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (fetchingUrl) {
|
if (fetchingUrl) {
|
||||||
@ -116,7 +132,8 @@ function handleClick(e) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
}).then(res => res.json()).then(res => {
|
}).then(res => res.json()).then(res => {
|
||||||
fetchingUrl = false;
|
fetchingUrl = false;
|
||||||
const err = res["error"];
|
const err = res["error"];
|
||||||
@ -134,9 +151,19 @@ function handleClick(e) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Array.from(loginButtons).forEach(el => {
|
Array.from(document.querySelectorAll(".js-loginButton")).forEach(el => {
|
||||||
el.addEventListener("click", handleClick);
|
el.addEventListener("click", function(e) {
|
||||||
|
data.Reauthenticate = true;
|
||||||
|
postData(e);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
Array.from(document.querySelectorAll(".js-advertiseExitNode")).forEach(el => {
|
||||||
|
el.addEventListener("click", function(e) {
|
||||||
|
data.AdvertiseExitNode = !advertiseExitNode;
|
||||||
|
postData(e);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
})();</script>
|
})();</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user