Compare commits

..

2 Commits

Author SHA1 Message Date
Chris Palmer
e230e617f8 clientupdate: return NOTREACHED for macsys
The work is done in Swift; this is now a documentation placeholder.

Updates #6995

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-08-15 13:08:50 -07:00
Chris Palmer
a690347e62 clientupdate: return success for macsys
The work is done in Swift; this is now a documentation placeholder.

Updates #6995

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-08-15 11:23:08 -07:00
39 changed files with 410 additions and 1576 deletions

1
.gitignore vendored
View File

@@ -38,7 +38,6 @@ cmd/tailscaled/tailscaled
# Ignore web client node modules
.vite/
client/web/node_modules
client/web/build
/gocross
/dist

View File

@@ -1 +1 @@
1.49.0
1.47.0

View File

@@ -10,7 +10,6 @@ type DNSConfig struct {
Domains []string `json:"domains"`
Nameservers []string `json:"nameservers"`
Proxied bool `json:"proxied"`
DNSFilterURL string `json:"DNSFilterURL"`
}
type DNSResolver struct {

View File

@@ -1,29 +1,4 @@
<!doctype html>
<html class="bg-gray-50">
<head>
<title>Tailscale</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
<link rel="stylesheet" type="text/css" href="/src/index.css" />
</head>
<body>
<div class="min-h-screen py-10 flex justify-center items-center" style="display: none">
<div class="max-w-md">
<h3 class="font-semibold text-lg mb-4">Your web browser is unsupported.</h3>
<p class="mb-2">
Update to a modern browser to access the Tailscale web client. You can use
<a class="link" href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>,
<a class="link" href="https://www.microsoft.com/en-us/edge" target="_blank">Edge</a>,
<a class="link" href="https://www.apple.com/safari/" target="_blank">Safari</a>,
or <a class="link" href="https://www.google.com/chrome/" target="_blank">Chrome</a>.</p>
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a></p>
</div>
</div>
<noscript>
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
</noscript>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
</html>

View File

@@ -17,8 +17,6 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"postcss": "^8.4.27",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2",
"tailwindcss": "^3.3.3",
"typescript": "^4.7.4",
"vite": "^4.3.9",
@@ -31,9 +29,7 @@
"build": "vite build",
"start": "vite",
"lint": "tsc --noEmit",
"test": "vitest",
"format": "prettier --write 'src/**/*.{ts,tsx}'",
"format-check": "prettier --check 'src/**/*.{ts,tsx}'"
"test": "vitest"
},
"prettier": {
"semi": false,

View File

@@ -1,25 +1,5 @@
import React from "react"
import { Footer, Header, IP, State } from "src/components/legacy"
import useNodeData from "src/hooks/node-data"
export default function App() {
const data = useNodeData()
return (
<div className="py-14">
{!data ? (
// TODO(sonia): add a loading view
<div className="text-center">Loading...</div>
) : (
<>
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<Header data={data} />
<IP data={data} />
<State data={data} />
</main>
<Footer data={data} />
</>
)}
</div>
)
return <div className="text-center">Hello world</div>
}

View File

@@ -1,272 +0,0 @@
import React from "react"
import { NodeData } from "src/hooks/node-data"
// TODO(tailscale/corp#13775): legacy.tsx contains a set of components
// that (crudely) implement the pre-2023 web client. These are implemented
// purely to ease migration to the new React-based web client, and will
// eventually be completely removed.
export function Header(props: { data: NodeData }) {
const { data } = props
return (
<header className="flex justify-between items-center min-width-0 py-2 mb-8">
<svg
width="26"
height="26"
viewBox="0 0 23 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="flex-shrink-0 mr-4"
>
<circle
opacity="0.2"
cx="3.4"
cy="3.25"
r="2.7"
fill="currentColor"
></circle>
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle
opacity="0.2"
cx="3.4"
cy="19.5"
r="2.7"
fill="currentColor"
></circle>
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle
opacity="0.2"
cx="11.5"
cy="3.25"
r="2.7"
fill="currentColor"
></circle>
<circle
opacity="0.2"
cx="19.5"
cy="3.25"
r="2.7"
fill="currentColor"
></circle>
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle
opacity="0.2"
cx="19.5"
cy="19.5"
r="2.7"
fill="currentColor"
></circle>
</svg>
<div className="flex items-center justify-end space-x-2 w-2/3">
{data.Profile && (
<>
<div className="text-right w-full leading-4">
<h4 className="truncate leading-normal">
{data.Profile.LoginName}
</h4>
<div className="text-xs text-gray-500 text-right">
<a href="#" className="hover:text-gray-700 js-loginButton">
Switch account
</a>{" "}
|{" "}
<a href="#" className="hover:text-gray-700 js-loginButton">
Reauthenticate
</a>{" "}
|{" "}
<a href="#" className="hover:text-gray-700 js-logoutButton">
Logout
</a>
</div>
</div>
<div className="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{data.Profile.ProfilePicURL ? (
<div
className="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style={{
backgroundImage: `url(${data.Profile.ProfilePicURL})`,
backgroundSize: "cover",
}}
/>
) : (
<div className="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed" />
)}
</div>
</>
)}
</div>
</header>
)
}
export function IP(props: { data: NodeData }) {
const { data } = props
if (!data.IP) {
return null
}
return (
<>
<div className="border border-gray-200 bg-gray-50 rounded-md p-2 pl-3 pr-3 width-full flex items-center justify-between">
<div className="flex items-center min-width-0">
<svg
className="flex-shrink-0 text-gray-600 mr-3 ml-1"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<div>
<h4 className="font-semibold truncate mr-2">{data.DeviceName}</h4>
</div>
</div>
<h5>{data.IP}</h5>
</div>
<p className="mt-1 ml-1 mb-6 text-xs text-gray-600">
Debug info: Tailscale {data.IPNVersion}, tun={data.TUNMode.toString()}
{data.IsSynology && (
<>
, DSM{data.DSMVersion}
{data.TUNMode || (
<>
{" "}
(
<a
href="https://tailscale.com/kb/1152/synology-outbound/"
className="link-underline text-gray-600"
target="_blank"
aria-label="Configure outbound synology traffic"
rel="noopener noreferrer"
>
outgoing access not configured
</a>
)
</>
)}
</>
)}
</p>
</>
)
}
export function State(props: { data: NodeData }) {
const { data } = props
switch (data.Status) {
case "NeedsLogin":
case "NoState":
if (data.IP) {
return (
<>
<div className="mb-6">
<p className="text-gray-700">
Your device's key has expired. Reauthenticate this device by
logging in again, or{" "}
<a
href="https://tailscale.com/kb/1028/key-expiry"
className="link"
target="_blank"
>
learn more
</a>
.
</p>
</div>
<a href="#" className="mb-4 js-loginButton" target="_blank">
<button className="button button-blue w-full">
Reauthenticate
</button>
</a>
</>
)
} else {
return (
<>
<div className="mb-6">
<h3 className="text-3xl font-semibold mb-3">Log in</h3>
<p className="text-gray-700">
Get started by logging in to your Tailscale network.
Or,&nbsp;learn&nbsp;more at{" "}
<a
href="https://tailscale.com/"
className="link"
target="_blank"
>
tailscale.com
</a>
.
</p>
</div>
<a href="#" className="mb-4 js-loginButton" target="_blank">
<button className="button button-blue w-full">Log In</button>
</a>
</>
)
}
case "NeedsMachineAuth":
return (
<div className="mb-4">
This device is authorized, but needs approval from a network admin
before it can connect to the network.
</div>
)
default:
return (
<>
<div className="mb-4">
<p>
You are connected! Access this device over Tailscale using the
device name or IP address above.
</p>
</div>
<div className="mb-4">
<a href="#" className="mb-4 js-advertiseExitNode">
{data.AdvertiseExitNode ? (
<button
className="button button-red button-medium"
id="enabled"
>
Stop advertising Exit Node
</button>
) : (
<button
className="button button-blue button-medium"
id="enabled"
>
Advertise as Exit Node
</button>
)}
</a>
</div>
</>
)
}
}
export function Footer(props: { data: NodeData }) {
const { data } = props
return (
<footer className="container max-w-lg mx-auto text-center">
<a
className="text-xs text-gray-500 hover:text-gray-600"
href={data.LicensesURL}
>
Open Source Licenses
</a>
</footer>
)
}

View File

@@ -1,37 +0,0 @@
import { useEffect, useState } from "react"
export type NodeData = {
Profile: UserProfile
Status: string
DeviceName: string
IP: string
AdvertiseExitNode: boolean
AdvertiseRoutes: string
LicensesURL: string
TUNMode: boolean
IsSynology: boolean
DSMVersion: number
IsUnraid: boolean
UnraidToken: string
IPNVersion: string
}
export type UserProfile = {
LoginName: string
DisplayName: string
ProfilePicURL: string
}
// useNodeData returns basic data about the current node.
export default function useNodeData() {
const [data, setData] = useState<NodeData>()
useEffect(() => {
fetch("/api/data")
.then((response) => response.json())
.then((json) => setData(json))
.catch((error) => console.error(error))
}, [])
return data
}

View File

@@ -1,130 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/**
* Non-Tailwind styles begin here.
*/
.bg-gray-0 {
--tw-bg-opacity: 1;
background-color: rgba(250, 249, 248, var(--tw-bg-opacity));
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgba(249, 247, 246, var(--tw-bg-opacity));
}
html {
letter-spacing: -0.015em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.link {
--text-opacity: 1;
color: #4b70cc;
color: rgba(75, 112, 204, var(--text-opacity));
}
.link:hover,
.link:active {
--text-opacity: 1;
color: #19224a;
color: rgba(25, 34, 74, var(--text-opacity));
}
.link-underline {
text-decoration: underline;
}
.link-underline:hover,
.link-underline:active {
text-decoration: none;
}
.link-muted {
/* same as text-gray-500 */
--tw-text-opacity: 1;
color: rgba(112, 110, 109, var(--tw-text-opacity));
}
.link-muted:hover,
.link-muted:active {
/* same as text-gray-500 */
--tw-text-opacity: 1;
color: rgba(68, 67, 66, var(--tw-text-opacity));
}
.button {
font-weight: 500;
padding-top: 0.45rem;
padding-bottom: 0.45rem;
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.375rem;
border-width: 1px;
border-color: transparent;
transition-property: background-color, border-color, color, box-shadow;
transition-duration: 120ms;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
min-width: 80px;
}
.button:focus {
outline: 0;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
.button:disabled {
cursor: not-allowed;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.button-blue {
--bg-opacity: 1;
background-color: #4b70cc;
background-color: rgba(75, 112, 204, var(--bg-opacity));
--border-opacity: 1;
border-color: #4b70cc;
border-color: rgba(75, 112, 204, var(--border-opacity));
--text-opacity: 1;
color: #fff;
color: rgba(255, 255, 255, var(--text-opacity));
}
.button-blue:enabled:hover {
--bg-opacity: 1;
background-color: #3f5db3;
background-color: rgba(63, 93, 179, var(--bg-opacity));
--border-opacity: 1;
border-color: #3f5db3;
border-color: rgba(63, 93, 179, var(--border-opacity));
}
.button-blue:disabled {
--text-opacity: 1;
color: #cedefd;
color: rgba(206, 222, 253, var(--text-opacity));
--bg-opacity: 1;
background-color: #6c94ec;
background-color: rgba(108, 148, 236, var(--bg-opacity));
--border-opacity: 1;
border-color: #6c94ec;
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;
}

View File

@@ -31,7 +31,6 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/util/groupmember"
"tailscale.com/util/httpm"
"tailscale.com/version/distro"
)
@@ -79,6 +78,30 @@ func init() {
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
// whether the user has access to the web UI. The function will write the
// error to the provided http.ResponseWriter.
@@ -271,26 +294,12 @@ req.send(null);
// ServeHTTP processes all requests for the Tailscale web client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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.
s.devProxy.ServeHTTP(w, r)
return
}
ctx := r.Context()
if authRedirect(w, r) {
return
}
@@ -300,49 +309,80 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
switch {
case r.URL.Path == "/redirect" || r.URL.Path == "/redirect/":
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" {
io.WriteString(w, authenticationRedirectHTML)
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)
if err != nil {
return nil, err
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
prefs, err := s.lc.GetPrefs(ctx)
if err != nil {
return nil, err
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Method == "POST" {
defer r.Body.Close()
var postData postedData
type mi map[string]any
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
mp := &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
WantRunningSet: true,
}
mp.Prefs.WantRunning = true
mp.Prefs.AdvertiseRoutes = routes
log.Printf("Doing edit: %v", mp.Pretty())
if _, err := s.lc.EditPrefs(ctx, mp); err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
var reauth, logout bool
if postData.Reauthenticate {
reauth = true
}
if postData.ForceLogout {
logout = true
}
log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
url, err := s.tailscaleUp(r.Context(), st, postData)
log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
if url != "" {
json.NewEncoder(w).Encode(mi{"url": url})
} else {
io.WriteString(w, "{}")
}
return
}
profile := st.User[st.Self.UserID]
deviceName := strings.Split(st.Self.DNSName, ".")[0]
versionShort := strings.Split(st.Version, "-")[0]
data := &nodeData{
data := tmplData{
SynologyUser: user,
Profile: profile,
Status: st.BackendState,
@@ -370,106 +410,16 @@ func (s *Server) getNodeData(ctx context.Context, user string) (*nodeData, error
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, *data); err != nil {
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
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
}
func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
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
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
mp := &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
WantRunningSet: true,
}
mp.Prefs.WantRunning = true
mp.Prefs.AdvertiseRoutes = routes
log.Printf("Doing edit: %v", mp.Pretty())
if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
var reauth, logout bool
if postData.Reauthenticate {
reauth = true
}
if postData.ForceLogout {
logout = true
}
log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
url, err := s.tailscaleUp(r.Context(), st, postData)
log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
if url != "" {
json.NewEncoder(w).Encode(mi{"url": url})
} else {
io.WriteString(w, "{}")
}
return
}
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) {
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData postedData) (authURL string, retErr error) {
if postData.ForceLogout {
if err := s.lc.Logout(ctx); err != nil {
return "", fmt.Errorf("Logout error: %w", err)

View File

@@ -1401,16 +1401,6 @@ postcss@^8.4.23, postcss@^8.4.27:
picocolors "^1.0.0"
source-map-js "^1.0.2"
prettier-plugin-organize-imports@^3.2.2:
version "3.2.3"
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.3.tgz#6b0141ac71f7ee9a673ce83e95456319e3a7cf0d"
integrity sha512-KFvk8C/zGyvUaE3RvxN2MhCLwzV6OBbFSkwZ2OamCrs9ZY4i5L77jQ/w4UmUr+lqX8qbaqVq6bZZkApn+IgJSg==
prettier@^2.5.1:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
pretty-format@^29.5.0:
version "29.6.2"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47"

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"log"
"net"
"net/netip"
"strings"
"time"
@@ -68,7 +67,7 @@ func startMeshWithHost(s *derp.Server, host string) error {
return d.DialContext(ctx, network, addr)
})
add := func(k key.NodePublic, _ netip.AddrPort) { s.AddPacketForwarder(k, c) }
add := func(k key.NodePublic) { s.AddPacketForwarder(k, c) }
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
return nil

View File

@@ -66,7 +66,7 @@ func runExitNodeList(ctx context.Context, args []string) error {
var peers []*ipnstate.PeerStatus
for _, ps := range st.Peer {
if !ps.ExitNodeOption {
// We only show exit nodes under the exit-node subcommand.
// We only show location based exit nodes.
continue
}

View File

@@ -138,7 +138,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/client/web
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/net/interfaces+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/net/netcheck+

View File

@@ -292,7 +292,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
@@ -412,7 +411,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/tcpip/stack+
bufio from compress/flate+
bytes from bufio+
cmp from slices
compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+
W compress/zlib from debug/pe
@@ -497,7 +495,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from tailscale.com/log/logheap+
runtime/trace from net/http/pprof
slices from tailscale.com/wgengine/magicsock
sort from compress/flate+
strconv from compress/flate+
strings from bufio+

View File

@@ -1110,16 +1110,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
}
nm := sess.netmapForResponse(&resp)
// Occasionally print the netmap header.
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
// Code elsewhere prints netmap diffs every time they are received.
now := c.clock.Now()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
}
if nm.SelfNode == nil {
c.logf("MapResponse lacked node")
return errors.New("MapResponse lacked node")
@@ -1139,6 +1129,15 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
nm.SelfNode.Capabilities = nil
}
// Occasionally print the netmap header.
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
// Code elsewhere prints netmap diffs every time they are received.
now := c.clock.Now()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
}
newPersist := persist.AsStruct()
newPersist.NodeID = nm.SelfNode.StableID
newPersist.UserProfile = nm.UserProfiles[nm.User]

View File

@@ -85,7 +85,7 @@ const (
// framePeerPresent is like framePeerGone, but for other
// members of the DERP region when they're meshed up together.
framePeerPresent = frameType(0x09) // 32B pub key of peer that's connected + optional 18B ip:port (16 byte IP + 2 byte BE uint16 port)
framePeerPresent = frameType(0x09) // 32B pub key of peer that's connected
// frameWatchConns is how one DERP node in a regional mesh
// subscribes to the others in the region.

View File

@@ -363,12 +363,7 @@ func (PeerGoneMessage) msg() {}
// PeerPresentMessage is a ReceivedMessage that indicates that the client
// is connected to the server. (Only used by trusted mesh clients)
type PeerPresentMessage struct {
// Key is the public key of the client.
Key key.NodePublic
// IPPort is the remote IP and port of the client.
IPPort netip.AddrPort
}
type PeerPresentMessage key.NodePublic
func (PeerPresentMessage) msg() {}
@@ -551,15 +546,8 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
continue
}
var msg PeerPresentMessage
msg.Key = key.NodePublicFromRaw32(mem.B(b[:keyLen]))
if n >= keyLen+16+2 {
msg.IPPort = netip.AddrPortFrom(
netip.AddrFrom16([16]byte(b[keyLen:keyLen+16])).Unmap(),
binary.BigEndian.Uint16(b[keyLen+16:keyLen+16+2]),
)
}
return msg, nil
pg := PeerPresentMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
return pg, nil
case frameRecvPacket:
var rp ReceivedPacket

View File

@@ -12,7 +12,6 @@ import (
crand "crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/binary"
"encoding/json"
"errors"
"expvar"
@@ -44,7 +43,6 @@ import (
"tailscale.com/tstime/rate"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/set"
"tailscale.com/version"
)
@@ -152,7 +150,7 @@ type Server struct {
closed bool
netConns map[Conn]chan struct{} // chan is closed when conn closes
clients map[key.NodePublic]clientSet
watchers set.Set[*sclient] // mesh peers
watchers map[*sclient]bool // mesh peer -> true
// clientsMesh tracks all clients in the cluster, both locally
// and to mesh peers. If the value is nil, that means the
// peer is only local (and thus in the clients Map, but not
@@ -221,7 +219,8 @@ func (s singleClient) ForeachClient(f func(*sclient)) { f(s.c) }
// All fields are guarded by Server.mu.
type dupClientSet struct {
// set is the set of connected clients for sclient.key.
set set.Set[*sclient]
// The values are all true.
set map[*sclient]bool
// last is the most recent addition to set, or nil if the most
// recent one has since disconnected and nobody else has send
@@ -262,7 +261,7 @@ func (s *dupClientSet) removeClient(c *sclient) bool {
trim := s.sendHistory[:0]
for _, v := range s.sendHistory {
if s.set.Contains(v) && (len(trim) == 0 || trim[len(trim)-1] != v) {
if s.set[v] && (len(trim) == 0 || trim[len(trim)-1] != v) {
trim = append(trim, v)
}
}
@@ -317,7 +316,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
clientsMesh: map[key.NodePublic]PacketForwarder{},
netConns: map[Conn]chan struct{}{},
memSys0: ms.Sys,
watchers: set.Set[*sclient]{},
watchers: map[*sclient]bool{},
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
avgQueueDuration: new(uint64),
tcpRtt: metrics.LabelMap{Label: "le"},
@@ -499,8 +498,8 @@ func (s *Server) registerClient(c *sclient) {
s.mu.Lock()
defer s.mu.Unlock()
curSet := s.clients[c.key]
switch curSet := curSet.(type) {
set := s.clients[c.key]
switch set := set.(type) {
case nil:
s.clients[c.key] = singleClient{c}
c.debugLogf("register single client")
@@ -508,14 +507,14 @@ func (s *Server) registerClient(c *sclient) {
s.dupClientKeys.Add(1)
s.dupClientConns.Add(2) // both old and new count
s.dupClientConnTotal.Add(1)
old := curSet.ActiveClient()
old := set.ActiveClient()
old.isDup.Store(true)
c.isDup.Store(true)
s.clients[c.key] = &dupClientSet{
last: c,
set: set.Set[*sclient]{
old: struct{}{},
c: struct{}{},
set: map[*sclient]bool{
old: true,
c: true,
},
sendHistory: []*sclient{old},
}
@@ -524,9 +523,9 @@ func (s *Server) registerClient(c *sclient) {
s.dupClientConns.Add(1) // the gauge
s.dupClientConnTotal.Add(1) // the counter
c.isDup.Store(true)
curSet.set.Add(c)
curSet.last = c
curSet.sendHistory = append(curSet.sendHistory, c)
set.set[c] = true
set.last = c
set.sendHistory = append(set.sendHistory, c)
c.debugLogf("register another duplicate client")
}
@@ -535,7 +534,7 @@ func (s *Server) registerClient(c *sclient) {
}
s.keyOfAddr[c.remoteIPPort] = c.key
s.curClients.Add(1)
s.broadcastPeerStateChangeLocked(c.key, c.remoteIPPort, true)
s.broadcastPeerStateChangeLocked(c.key, true)
}
// broadcastPeerStateChangeLocked enqueues a message to all watchers
@@ -543,13 +542,9 @@ func (s *Server) registerClient(c *sclient) {
// presence changed.
//
// s.mu must be held.
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, ipPort netip.AddrPort, present bool) {
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, present bool) {
for w := range s.watchers {
w.peerStateChange = append(w.peerStateChange, peerConnState{
peer: peer,
present: present,
ipPort: ipPort,
})
w.peerStateChange = append(w.peerStateChange, peerConnState{peer: peer, present: present})
go w.requestMeshUpdate()
}
}
@@ -570,7 +565,7 @@ func (s *Server) unregisterClient(c *sclient) {
delete(s.clientsMesh, c.key)
s.notePeerGoneFromRegionLocked(c.key)
}
s.broadcastPeerStateChangeLocked(c.key, netip.AddrPort{}, false)
s.broadcastPeerStateChangeLocked(c.key, false)
case *dupClientSet:
c.debugLogf("removed duplicate client")
if set.removeClient(c) {
@@ -660,21 +655,13 @@ func (s *Server) addWatcher(c *sclient) {
defer s.mu.Unlock()
// Queue messages for each already-connected client.
for peer, clientSet := range s.clients {
ac := clientSet.ActiveClient()
if ac == nil {
continue
}
c.peerStateChange = append(c.peerStateChange, peerConnState{
peer: peer,
present: true,
ipPort: ac.remoteIPPort,
})
for peer := range s.clients {
c.peerStateChange = append(c.peerStateChange, peerConnState{peer: peer, present: true})
}
// And enroll the watcher in future updates (of both
// connections & disconnections).
s.watchers.Add(c)
s.watchers[c] = true
go c.requestMeshUpdate()
}
@@ -1362,7 +1349,6 @@ type sclient struct {
type peerConnState struct {
peer key.NodePublic
present bool
ipPort netip.AddrPort // if present, the peer's IP:port
}
// pkt is a request to write a data frame to an sclient.
@@ -1556,18 +1542,12 @@ func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) e
}
// sendPeerPresent sends a peerPresent frame, without flushing.
func (c *sclient) sendPeerPresent(peer key.NodePublic, ipPort netip.AddrPort) error {
func (c *sclient) sendPeerPresent(peer key.NodePublic) error {
c.setWriteDeadline()
const frameLen = keyLen + 16 + 2
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, frameLen); err != nil {
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, keyLen); err != nil {
return err
}
payload := make([]byte, frameLen)
_ = peer.AppendTo(payload[:0])
a16 := ipPort.Addr().As16()
copy(payload[keyLen:], a16[:])
binary.BigEndian.PutUint16(payload[keyLen+16:], ipPort.Port())
_, err := c.bw.Write(payload)
_, err := c.bw.Write(peer.AppendTo(nil))
return err
}
@@ -1586,7 +1566,7 @@ func (c *sclient) sendMeshUpdates() error {
}
var err error
if pcs.present {
err = c.sendPeerPresent(pcs.peer, pcs.ipPort)
err = c.sendPeerPresent(pcs.peer)
} else {
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
}

View File

@@ -92,7 +92,7 @@ func TestSendRecv(t *testing.T) {
defer cancel()
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
go s.Accept(ctx, cin, brwServer, fmt.Sprintf("[abc::def]:%v", i))
go s.Accept(ctx, cin, brwServer, fmt.Sprintf("test-client-%d", i))
key := clientPrivateKeys[i]
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
@@ -528,7 +528,7 @@ func newTestServer(t *testing.T, ctx context.Context) *testServer {
// TODO: register c in ts so Close also closes it?
go func(i int) {
brwServer := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
go s.Accept(ctx, c, brwServer, c.RemoteAddr().String())
go s.Accept(ctx, c, brwServer, fmt.Sprintf("test-client-%d", i))
}(i)
}
}()
@@ -615,7 +615,7 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) {
}
switch m := m.(type) {
case PeerPresentMessage:
got := m.Key
got := key.NodePublic(m)
if !want[got] {
t.Fatalf("got peer present for %v; want present for %v", tc.ts.keyName(got), logger.ArgWriter(func(bw *bufio.Writer) {
for _, pub := range peers {
@@ -623,7 +623,6 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) {
}
}))
}
t.Logf("got present with IP %v", m.IPPort)
delete(want, got)
if len(want) == 0 {
return

View File

@@ -5,7 +5,6 @@ package derphttp
import (
"context"
"net/netip"
"sync"
"time"
@@ -27,7 +26,7 @@ import (
//
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
// be closed, and c itself needs to be closed.
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add func(key.NodePublic, netip.AddrPort), remove func(key.NodePublic)) {
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add, remove func(key.NodePublic)) {
if infoLogf == nil {
infoLogf = logger.Discard
}
@@ -69,9 +68,9 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
})
defer timer.Stop()
updatePeer := func(k key.NodePublic, ipPort netip.AddrPort, isPresent bool) {
updatePeer := func(k key.NodePublic, isPresent bool) {
if isPresent {
add(k, ipPort)
add(k)
} else {
remove(k)
}
@@ -127,7 +126,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
switch m := m.(type) {
case derp.PeerPresentMessage:
updatePeer(m.Key, m.IPPort, true)
updatePeer(key.NodePublic(m), true)
case derp.PeerGoneMessage:
switch m.Reason {
case derp.PeerGoneReasonDisconnected:
@@ -139,7 +138,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
logf("Recv: peer %s not at server %s for unknown reason %v",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString(), m.Reason)
}
updatePeer(key.NodePublic(m.Peer), netip.AddrPort{}, false)
updatePeer(key.NodePublic(m.Peer), false)
default:
continue
}

View File

@@ -171,7 +171,6 @@ var (
desktopAtomic atomic.Value // of opt.Bool
packagingType atomic.Value // of string
appType atomic.Value // of string
firewallMode atomic.Value // of string
)
// SetPushDeviceToken sets the device token for use in Hostinfo updates.
@@ -183,9 +182,6 @@ func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
// SetOSVersion sets the OS version.
func SetOSVersion(v string) { osVersionAtomic.Store(v) }
// SetFirewallMode sets the firewall mode for the app.
func SetFirewallMode(v string) { firewallMode.Store(v) }
// SetPackage sets the packaging type for the app.
//
// As of 2022-03-25, this is used by Android ("nogoogle" for the
@@ -207,13 +203,6 @@ func pushDeviceToken() string {
return s
}
// FirewallMode returns the firewall mode for the app.
// It is empty if unset.
func FirewallMode() string {
s, _ := firewallMode.Load().(string)
return s
}
func desktop() (ret opt.Bool) {
if runtime.GOOS != "linux" {
return opt.Bool("")

View File

@@ -64,7 +64,6 @@ const (
NotifyInitialState // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
NotifyGUINetMap // if set, only use the Notify.GUINetMap; Notify.Netmap will always be nil. Also impacts NotifyInitialNetMap.
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
)
@@ -82,14 +81,13 @@ type Notify struct {
// For State InUseOtherUser, ErrMessage is not critical and just contains the details.
ErrMessage *string
LoginFinished *empty.Message // non-nil when/if the login process succeeded
State *State // if non-nil, the new or current IPN state
Prefs *PrefsView // if non-nil && Valid, the new or current preferences
//NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
GUINetMap *netmap.GUINetworkMap // if non-nil, the new or current netmap
Engine *EngineStatus // if non-nil, the new or current wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
BackendLogID *string // if non-nil, the public logtail ID used by backend
LoginFinished *empty.Message // non-nil when/if the login process succeeded
State *State // if non-nil, the new or current IPN state
Prefs *PrefsView // if non-nil && Valid, the new or current preferences
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
Engine *EngineStatus // if non-nil, the new or current wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
BackendLogID *string // if non-nil, the public logtail ID used by backend
// FilesWaiting if non-nil means that files are buffered in
// the Tailscale daemon and ready for local transfer to the
@@ -135,9 +133,9 @@ func (n Notify) String() string {
if n.Prefs != nil && n.Prefs.Valid() {
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
}
// if n.NetMap != nil {
// sb.WriteString("NetMap{...} ")
// }
if n.NetMap != nil {
sb.WriteString("NetMap{...} ")
}
if n.Engine != nil {
fmt.Fprintf(&sb, "wg=%v ", *n.Engine)
}

View File

@@ -29,6 +29,8 @@ type strideEntry[T any] struct {
prefixIndex int
// value is the value associated with the strideEntry, if any.
value *T
// child is the child strideTable associated with the strideEntry, if any.
child *strideTable[T]
}
// strideTable is a binary tree that implements an 8-bit routing table.
@@ -48,17 +50,12 @@ type strideTable[T any] struct {
// parent of the node at index i is located at index i>>1, and its children
// at indices i<<1 and (i<<1)+1.
//
// A few consequences of this arrangement: host routes (/8) occupy
// the last numChildren entries in the table; the single default
// route /0 is at index 1, and index 0 is unused (in the original
// paper, it's hijacked through sneaky C memory trickery to store
// the refcount, but this is Go, where we don't store random bits
// in pointers lest we confuse the GC)
// A few consequences of this arrangement: host routes (/8) occupy the last
// 256 entries in the table; the single default route /0 is at index 1, and
// index 0 is unused (in the original paper, it's hijacked through sneaky C
// memory trickery to store the refcount, but this is Go, where we don't
// store random bits in pointers lest we confuse the GC)
entries [lastHostIndex + 1]strideEntry[T]
// children are the child tables of this table. Each child
// represents the address space within one of this table's host
// routes (/8).
children [numChildren]*strideTable[T]
// routeRefs is the number of route entries in this table.
routeRefs uint16
// childRefs is the number of child strideTables referenced by this table.
@@ -70,60 +67,63 @@ const (
firstHostIndex = 0b1_0000_0000
// lastHostIndex is the array index of the last host route. This is hostIndex(0xFF/8).
lastHostIndex = 0b1_1111_1111
// numChildren is the maximum number of child tables a strideTable can hold.
numChildren = 256
)
// getChild returns the child strideTable pointer for addr, or nil if none.
func (t *strideTable[T]) getChild(addr uint8) *strideTable[T] {
return t.children[addr]
// getChild returns the child strideTable pointer for addr (if any), and an
// internal array index that can be used with deleteChild.
func (t *strideTable[T]) getChild(addr uint8) (child *strideTable[T], idx int) {
idx = hostIndex(addr)
return t.entries[idx].child, idx
}
// deleteChild deletes the child strideTable at addr. It is valid to
// delete a non-existent child.
func (t *strideTable[T]) deleteChild(addr uint8) {
if t.children[addr] != nil {
t.childRefs--
}
t.children[addr] = nil
// deleteChild deletes the child strideTable at idx (if any). idx should be
// obtained via a call to getChild.
func (t *strideTable[T]) deleteChild(idx int) {
t.entries[idx].child = nil
t.childRefs--
}
// setChild sets the child strideTable for addr to child.
// setChild replaces the child strideTable for addr (if any) with child.
func (t *strideTable[T]) setChild(addr uint8, child *strideTable[T]) {
if t.children[addr] == nil {
t.setChildByIndex(hostIndex(addr), child)
}
// setChildByIndex replaces the child strideTable at idx (if any) with
// child. idx should be obtained via a call to getChild.
func (t *strideTable[T]) setChildByIndex(idx int, child *strideTable[T]) {
if t.entries[idx].child == nil {
t.childRefs++
}
t.children[addr] = child
t.entries[idx].child = child
}
// getOrCreateChild returns the child strideTable for addr, creating it if
// necessary.
func (t *strideTable[T]) getOrCreateChild(addr uint8) (child *strideTable[T], created bool) {
ret := t.children[addr]
if ret == nil {
ret = &strideTable[T]{
idx := hostIndex(addr)
if t.entries[idx].child == nil {
t.entries[idx].child = &strideTable[T]{
prefix: childPrefixOf(t.prefix, addr),
}
t.children[addr] = ret
t.childRefs++
return ret, true
return t.entries[idx].child, true
}
return ret, false
return t.entries[idx].child, false
}
// getValAndChild returns both the prefix and child strideTable for
// addr. Both returned values can be nil if no entry of that type
// exists for addr.
func (t *strideTable[T]) getValAndChild(addr uint8) (*T, *strideTable[T]) {
return t.entries[hostIndex(addr)].value, t.children[addr]
idx := hostIndex(addr)
return t.entries[idx].value, t.entries[idx].child
}
// findFirstChild returns the first child strideTable in t, or nil if
// t has no children.
func (t *strideTable[T]) findFirstChild() *strideTable[T] {
for _, child := range t.children {
if child != nil {
for i := firstHostIndex; i <= lastHostIndex; i++ {
if child := t.entries[i].child; child != nil {
return child
}
}

View File

@@ -364,7 +364,7 @@ func (t *Table[T]) Delete(pfx netip.Prefix) {
// write to strideTables[N] and strideIndexes[N-1].
strideIdx := 0
strideTables := [16]*strideTable[T]{st}
strideIndexes := [15]uint8{}
strideIndexes := [15]int{}
// Similar to Insert, navigate down the tree of strideTables,
// looking for the one that houses this prefix. This part is
@@ -384,7 +384,7 @@ func (t *Table[T]) Delete(pfx netip.Prefix) {
if debugDelete {
fmt.Printf("delete: loop byteIdx=%d numBits=%d st.prefix=%s\n", byteIdx, numBits, st.prefix)
}
child := st.getChild(bs[byteIdx])
child, idx := st.getChild(bs[byteIdx])
if child == nil {
// Prefix can't exist in the table, because one of the
// necessary strideTables doesn't exist.
@@ -393,7 +393,7 @@ func (t *Table[T]) Delete(pfx netip.Prefix) {
}
return
}
strideIndexes[strideIdx] = bs[byteIdx]
strideIndexes[strideIdx] = idx
strideTables[strideIdx+1] = child
strideIdx++
@@ -475,7 +475,7 @@ func (t *Table[T]) Delete(pfx netip.Prefix) {
if debugDelete {
fmt.Printf("delete: compact parent.prefix=%s st.prefix=%s child.prefix=%s\n", parent.prefix, cur.prefix, child.prefix)
}
strideTables[strideIdx-1].setChild(strideIndexes[strideIdx-1], child)
strideTables[strideIdx-1].setChildByIndex(strideIndexes[strideIdx-1], child)
return
default:
// This table has two or more children, so it's acting as a "fork in
@@ -505,12 +505,12 @@ func strideSummary[T any](w io.Writer, st *strideTable[T], indent int) {
fmt.Fprintf(w, "%s: %d routes, %d children\n", st.prefix, st.routeRefs, st.childRefs)
indent += 4
st.treeDebugStringRec(w, 1, indent)
for addr, child := range st.children {
if child == nil {
continue
for i := firstHostIndex; i <= lastHostIndex; i++ {
if child := st.entries[i].child; child != nil {
addr, len := inversePrefixIndex(i)
fmt.Fprintf(w, "%s%d/%d (%02x/%d): ", strings.Repeat(" ", indent), addr, len, addr, len)
strideSummary(w, child, indent)
}
fmt.Fprintf(w, "%s%d/8 (%02x/8): ", strings.Repeat(" ", indent), addr, addr)
strideSummary(w, child, indent)
}
}

View File

@@ -607,7 +607,7 @@ func TestInsertCompare(t *testing.T) {
seenVals4[fastVal] = true
}
if slowVal != fastVal {
t.Fatalf("get(%q) = %p, want %p", a, fastVal, slowVal)
t.Errorf("get(%q) = %p, want %p", a, fastVal, slowVal)
}
}
@@ -1092,12 +1092,11 @@ func (t *Table[T]) numStridesRec(seen map[*strideTable[T]]bool, st *strideTable[
if st.childRefs == 0 {
return ret
}
for _, c := range st.children {
if c == nil || seen[c] {
continue
for i := firstHostIndex; i <= lastHostIndex; i++ {
if c := st.entries[i].child; c != nil && !seen[c] {
seen[c] = true
ret += t.numStridesRec(seen, c)
}
seen[c] = true
ret += t.numStridesRec(seen, c)
}
return ret
}

View File

@@ -734,11 +734,6 @@ type NetInfo struct {
// the control plane.
DERPLatency map[string]float64 `json:",omitempty"`
// FirewallMode is the current firewall utility in use by router (iptables, nftables).
// FirewallMode ipt means iptables, nft means nftables. When it's empty user is not using
// our netfilter runners to manage firewall rules.
FirewallMode string `json:",omitempty"`
// Update BasicallyEqual when adding fields.
}
@@ -746,10 +741,10 @@ func (ni *NetInfo) String() string {
if ni == nil {
return "NetInfo(nil)"
}
return fmt.Sprintf("NetInfo{varies=%v hairpin=%v ipv6=%v ipv6os=%v udp=%v icmpv4=%v derp=#%v portmap=%v link=%q firewallmode=%q}",
return fmt.Sprintf("NetInfo{varies=%v hairpin=%v ipv6=%v ipv6os=%v udp=%v icmpv4=%v derp=#%v portmap=%v link=%q}",
ni.MappingVariesByDestIP, ni.HairPinning, ni.WorkingIPv6,
ni.OSHasIPv6, ni.WorkingUDP, ni.WorkingICMPv4,
ni.PreferredDERP, ni.portMapSummary(), ni.LinkType, ni.FirewallMode)
ni.PreferredDERP, ni.portMapSummary(), ni.LinkType)
}
func (ni *NetInfo) portMapSummary() string {
@@ -797,8 +792,7 @@ func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool {
ni.PMP == ni2.PMP &&
ni.PCP == ni2.PCP &&
ni.PreferredDERP == ni2.PreferredDERP &&
ni.LinkType == ni2.LinkType &&
ni.FirewallMode == ni2.FirewallMode
ni.LinkType == ni2.LinkType
}
// Equal reports whether h and h2 are equal.
@@ -1403,8 +1397,6 @@ type DNSConfig struct {
//
// Matches are case insensitive.
ExitNodeFilteredSet []string `json:",omitempty"`
// DNSFilterURL contains a user inputed URL that should have a list of domains to be blocked
DNSFilterURL string `json:",omitempty"`
}
// DNSRecord is an extra DNS record to add to MagicDNS.

View File

@@ -195,7 +195,6 @@ var _NetInfoCloneNeedsRegeneration = NetInfo(struct {
PreferredDERP int
LinkType string
DERPLatency map[string]float64
FirewallMode string
}{})
// Clone makes a deep copy of Login.
@@ -261,7 +260,6 @@ var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
CertDomains []string
ExtraRecords []DNSRecord
ExitNodeFilteredSet []string
DNSFilterURL string
}{})
// Clone makes a deep copy of RegisterResponse.

View File

@@ -571,7 +571,6 @@ func TestNetInfoFields(t *testing.T) {
"PreferredDERP",
"LinkType",
"DERPLatency",
"FirewallMode",
}
if have := fieldsOf(reflect.TypeOf(NetInfo{})); !reflect.DeepEqual(have, handled) {
t.Errorf("NetInfo.Clone/BasicallyEqually check might be out of sync\nfields: %q\nhandled: %q\n",

View File

@@ -408,7 +408,6 @@ func (v NetInfoView) PreferredDERP() int { return v.ж.PreferredDER
func (v NetInfoView) LinkType() string { return v.ж.LinkType }
func (v NetInfoView) DERPLatency() views.Map[string, float64] { return views.MapOf(v.ж.DERPLatency) }
func (v NetInfoView) FirewallMode() string { return v.ж.FirewallMode }
func (v NetInfoView) String() string { return v.ж.String() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@@ -426,7 +425,6 @@ var _NetInfoViewNeedsRegeneration = NetInfo(struct {
PreferredDERP int
LinkType string
DERPLatency map[string]float64
FirewallMode string
}{})
// View returns a readonly view of Login.
@@ -557,7 +555,6 @@ func (v DNSConfigView) ExtraRecords() views.Slice[DNSRecord] { return views.Slic
func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] {
return views.SliceOf(v.ж.ExitNodeFilteredSet)
}
func (v DNSConfigView) DNSFilterURL() string { return v.ж.DNSFilterURL }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
@@ -570,7 +567,6 @@ var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
CertDomains []string
ExtraRecords []DNSRecord
ExitNodeFilteredSet []string
DNSFilterURL string
}{})
// View returns a readonly view of RegisterResponse.

View File

@@ -45,12 +45,6 @@ type AccessLogRecord struct {
Bytes int `json:"bytes,omitempty"`
// Error encountered during request processing.
Err string `json:"err,omitempty"`
// RequestID is a unique ID for this request. When a request fails due to an
// error, the ID is generated and displayed to the client immediately after
// the error text, as well as logged here. This makes it easier to correlate
// support requests with server logs. If a RequestID generator is not
// configured, RequestID will be empty.
RequestID RequestID `json:"request_id,omitempty"`
}
// String returns m as a JSON string.

View File

@@ -169,8 +169,7 @@ type ReturnHandler interface {
type HandlerOptions struct {
QuietLoggingIfSuccessful bool // if set, do not log successfully handled HTTP requests (200 and 304 status codes)
Logf logger.Logf
Now func() time.Time // if nil, defaults to time.Now
GenerateRequestID func(*http.Request) RequestID // if nil, no request IDs are generated
Now func() time.Time // if nil, defaults to time.Now
// If non-nil, StatusCodeCounters maintains counters
// of status codes for handled responses.
@@ -267,11 +266,6 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
msg.Code = 499 // nginx convention: Client Closed Request
msg.Err = context.Canceled.Error()
case hErrOK:
if hErr.RequestID == "" && h.opts.GenerateRequestID != nil {
hErr.RequestID = h.opts.GenerateRequestID(r)
}
msg.RequestID = hErr.RequestID
// Handler asked us to send an error. Do so, if we haven't
// already sent a response.
msg.Err = hErr.Msg
@@ -302,24 +296,14 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
lw.WriteHeader(msg.Code)
fmt.Fprintln(lw, hErr.Msg)
if hErr.RequestID != "" {
fmt.Fprintln(lw, hErr.RequestID)
}
}
case err != nil:
const internalServerError = "internal server error"
errorMessage := internalServerError
if h.opts.GenerateRequestID != nil {
msg.RequestID = h.opts.GenerateRequestID(r)
errorMessage = errorMessage + "\n" + string(msg.RequestID)
}
// Handler returned a generic error. Serve an internal server
// error, if necessary.
msg.Err = err.Error()
if lw.code == 0 {
msg.Code = http.StatusInternalServerError
http.Error(lw, errorMessage, msg.Code)
http.Error(lw, "internal server error", msg.Code)
}
}
@@ -414,44 +398,18 @@ func (l loggingResponseWriter) Flush() {
f.Flush()
}
// RequestID is an opaque identifier for a HTTP request, used to correlate
// user-visible errors with backend server logs. If present in a HTTPError, the
// RequestID will be printed alongside the message text and logged in the
// AccessLogRecord. If an HTTPError has no RequestID (or a non-HTTPError error
// is returned), but the StdHandler has a RequestID generator function, then a
// RequestID will be generated before responding to the client and logging the
// error.
//
// In the event that there is no ErrorHandlerFunc and a non-HTTPError is
// returned to a StdHandler, the response body will be formatted like
// "internal server error\n{RequestID}\n".
//
// There is no particular format required for a RequestID, but ideally it should
// be obvious to an end-user that it is something to record for support
// purposes. One possible example for a RequestID format is:
// REQ-{server identifier}-{timestamp}-{random hex string}.
type RequestID string
// HTTPError is an error with embedded HTTP response information.
//
// It is the error type to be (optionally) used by Handler.ServeHTTPReturn.
type HTTPError struct {
Code int // HTTP response code to send to client; 0 means 500
Msg string // Response body to send to client
Err error // Detailed error to log on the server
RequestID RequestID // Optional identifier to connect client-visible errors with server logs
Header http.Header // Optional set of HTTP headers to set in the response
Code int // HTTP response code to send to client; 0 means 500
Msg string // Response body to send to client
Err error // Detailed error to log on the server
Header http.Header // Optional set of HTTP headers to set in the response
}
// Error implements the error interface.
func (e HTTPError) Error() string {
if e.RequestID != "" {
return fmt.Sprintf("httperror{%d, %q, %v, RequestID=%q}", e.Code, e.Msg, e.Err, e.RequestID)
} else {
// Backwards compatibility
return fmt.Sprintf("httperror{%d, %q, %v}", e.Code, e.Msg, e.Err)
}
}
func (e HTTPError) Error() string { return fmt.Sprintf("httperror{%d, %q, %v}", e.Code, e.Msg, e.Err) }
func (e HTTPError) Unwrap() error { return e.Err }

View File

@@ -38,7 +38,6 @@ func (f handlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) err
}
func TestStdHandler(t *testing.T) {
const exampleRequestID = "example-request-id"
var (
handlerCode = func(code int) ReturnHandler {
return handlerFunc(func(w http.ResponseWriter, r *http.Request) error {
@@ -67,20 +66,16 @@ func TestStdHandler(t *testing.T) {
bgCtx = context.Background()
// canceledCtx, cancel = context.WithCancel(bgCtx)
startTime = time.Unix(1687870000, 1234)
setExampleRequestID = func(_ *http.Request) RequestID { return exampleRequestID }
)
// cancel()
tests := []struct {
name string
rh ReturnHandler
r *http.Request
errHandler ErrorHandlerFunc
generateRequestID func(*http.Request) RequestID
wantCode int
wantLog AccessLogRecord
wantBody string
name string
rh ReturnHandler
r *http.Request
errHandler ErrorHandlerFunc
wantCode int
wantLog AccessLogRecord
}{
{
name: "handler returns 200",
@@ -99,24 +94,6 @@ func TestStdHandler(t *testing.T) {
},
},
{
name: "handler returns 200 with request ID",
rh: handlerCode(200),
r: req(bgCtx, "http://example.com/"),
generateRequestID: setExampleRequestID,
wantCode: 200,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
TLS: false,
Host: "example.com",
Method: "GET",
Code: 200,
RequestURI: "/",
},
},
{
name: "handler returns 404",
rh: handlerCode(404),
@@ -133,23 +110,6 @@ func TestStdHandler(t *testing.T) {
},
},
{
name: "handler returns 404 with request ID",
rh: handlerCode(404),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 404,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Code: 404,
},
},
{
name: "handler returns 404 via HTTPError",
rh: handlerErr(0, Error(404, "not found", testErr)),
@@ -165,27 +125,6 @@ func TestStdHandler(t *testing.T) {
Err: "not found: " + testErr.Error(),
Code: 404,
},
wantBody: "not found\n",
},
{
name: "handler returns 404 via HTTPError with request ID",
rh: handlerErr(0, Error(404, "not found", testErr)),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 404,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: "not found: " + testErr.Error(),
Code: 404,
RequestID: exampleRequestID,
},
wantBody: "not found\n" + exampleRequestID + "\n",
},
{
@@ -203,27 +142,6 @@ func TestStdHandler(t *testing.T) {
Err: "not found",
Code: 404,
},
wantBody: "not found\n",
},
{
name: "handler returns 404 with request ID and nil child error",
rh: handlerErr(0, Error(404, "not found", nil)),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 404,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: "not found",
Code: 404,
RequestID: exampleRequestID,
},
wantBody: "not found\n" + exampleRequestID + "\n",
},
{
@@ -241,27 +159,6 @@ func TestStdHandler(t *testing.T) {
Err: "visible error",
Code: 500,
},
wantBody: "visible error\n",
},
{
name: "handler returns user-visible error with request ID",
rh: handlerErr(0, vizerror.New("visible error")),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 500,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: "visible error",
Code: 500,
RequestID: exampleRequestID,
},
wantBody: "visible error\n" + exampleRequestID + "\n",
},
{
@@ -279,27 +176,6 @@ func TestStdHandler(t *testing.T) {
Err: "visible error",
Code: 500,
},
wantBody: "visible error\n",
},
{
name: "handler returns user-visible error wrapped by private error with request ID",
rh: handlerErr(0, fmt.Errorf("private internal error: %w", vizerror.New("visible error"))),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 500,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: "visible error",
Code: 500,
RequestID: exampleRequestID,
},
wantBody: "visible error\n" + exampleRequestID + "\n",
},
{
@@ -317,27 +193,6 @@ func TestStdHandler(t *testing.T) {
Err: testErr.Error(),
Code: 500,
},
wantBody: "internal server error\n",
},
{
name: "handler returns generic error with request ID",
rh: handlerErr(0, testErr),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 500,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: testErr.Error(),
Code: 500,
RequestID: exampleRequestID,
},
wantBody: "internal server error\n" + exampleRequestID + "\n",
},
{
@@ -357,25 +212,6 @@ func TestStdHandler(t *testing.T) {
},
},
{
name: "handler returns error after writing response with request ID",
rh: handlerErr(200, testErr),
r: req(bgCtx, "http://example.com/foo"),
generateRequestID: setExampleRequestID,
wantCode: 200,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
Host: "example.com",
Method: "GET",
RequestURI: "/foo",
Err: testErr.Error(),
Code: 200,
RequestID: exampleRequestID,
},
},
{
name: "handler returns HTTPError after writing response",
rh: handlerErr(200, Error(404, "not found", testErr)),
@@ -431,7 +267,6 @@ func TestStdHandler(t *testing.T) {
Code: 101,
},
},
{
name: "error handler gets run",
rh: handlerErr(0, Error(404, "not found", nil)), // status code changed in errHandler
@@ -451,62 +286,6 @@ func TestStdHandler(t *testing.T) {
Err: "not found",
RequestURI: "/",
},
wantBody: "not found\n",
},
{
name: "error handler gets run with request ID",
rh: handlerErr(0, Error(404, "not found", nil)), // status code changed in errHandler
r: req(bgCtx, "http://example.com/"),
generateRequestID: setExampleRequestID,
wantCode: 200,
errHandler: func(w http.ResponseWriter, r *http.Request, e HTTPError) {
http.Error(w, fmt.Sprintf("%s with request ID %s", e.Msg, e.RequestID), 200)
},
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
TLS: false,
Host: "example.com",
Method: "GET",
Code: 404,
Err: "not found",
RequestURI: "/",
RequestID: exampleRequestID,
},
wantBody: "not found with request ID " + exampleRequestID + "\n",
},
{
name: "request ID can use information from request",
rh: handlerErr(0, Error(400, "bad request", nil)),
r: func() *http.Request {
r := req(bgCtx, "http://example.com/")
r.AddCookie(&http.Cookie{Name: "want_request_id", Value: "asdf1234"})
return r
}(),
generateRequestID: func(r *http.Request) RequestID {
c, _ := r.Cookie("want_request_id")
if c == nil {
return ""
}
return RequestID(c.Value)
},
wantCode: 400,
wantLog: AccessLogRecord{
When: startTime,
Seconds: 1.0,
Proto: "HTTP/1.1",
TLS: false,
Host: "example.com",
RequestURI: "/",
Method: "GET",
Code: 400,
Err: "bad request",
RequestID: "asdf1234",
},
wantBody: "bad request\nasdf1234\n",
},
}
@@ -526,7 +305,7 @@ func TestStdHandler(t *testing.T) {
})
rec := noopHijacker{httptest.NewRecorder(), false}
h := StdHandler(test.rh, HandlerOptions{Logf: logf, Now: clock.Now, GenerateRequestID: test.generateRequestID, OnError: test.errHandler})
h := StdHandler(test.rh, HandlerOptions{Logf: logf, Now: clock.Now, OnError: test.errHandler})
h.ServeHTTP(&rec, test.r)
res := rec.Result()
if res.StatusCode != test.wantCode {
@@ -545,9 +324,6 @@ func TestStdHandler(t *testing.T) {
if diff := cmp.Diff(logs[0], test.wantLog, errTransform); diff != "" {
t.Errorf("handler wrote incorrect request log (-got+want):\n%s", diff)
}
if diff := cmp.Diff(rec.Body.String(), test.wantBody); diff != "" {
t.Errorf("handler wrote incorrect body (-got+want):\n%s", diff)
}
})
}
}

View File

@@ -19,34 +19,6 @@ import (
"tailscale.com/wgengine/filter"
)
type NetworkMapView struct {
nm *NetworkMap
}
type GUIPeerNode struct {
ID tailcfg.StableNodeID
BaseOrFQDNName string // no "." substring if in your tailnet, else FQDN
Owner tailcfg.UserID // user or fake userid for tagged nodes
IPv4 netip.Addr // may be be zero value (empty string in JSON)
IPv6 netip.Addr // may be be zero value (empty string in JSON)
MachineStatus tailcfg.MachineStatus
Hostinfo GUIHostInfo
IsExitNode bool
}
type GUIHostInfo struct {
ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user
}
type GUINetworkMap struct {
SelfNode *tailcfg.Node // TODO: GUISelfNode ?
Peers []*GUIPeerNode
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
TKAEnabled bool
}
// NetworkMap is the current state of the world.
//
// The fields should all be considered read-only. They might
@@ -98,6 +70,8 @@ type NetworkMap struct {
// hash of the latest update message to tick through TKA).
TKAHead tka.AUMHash
// ACLs
User tailcfg.UserID
// Domain is the current Tailnet name.

View File

@@ -1,248 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"net/netip"
"slices"
"sync"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/heap"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
const (
// endpointTrackerLifetime is how long we continue advertising an
// endpoint after we last see it. This is intentionally chosen to be
// slightly longer than a full netcheck period.
endpointTrackerLifetime = 5*time.Minute + 10*time.Second
// endpointTrackerMaxPerAddr is how many cached addresses we track for
// a given netip.Addr. This allows e.g. restricting the number of STUN
// endpoints we cache (which usually have the same netip.Addr but
// different ports).
//
// The value of 6 is chosen because we can advertise up to 3 endpoints
// based on the STUN IP:
// 1. The STUN endpoint itself (EndpointSTUN)
// 2. The STUN IP with the local Tailscale port (EndpointSTUN4LocalPort)
// 3. The STUN IP with a portmapped port (EndpointPortmapped)
//
// Storing 6 endpoints in the cache means we can store up to 2 previous
// sets of endpoints.
endpointTrackerMaxPerAddr = 6
)
// endpointTrackerEntry is an entry in an endpointHeap that stores the state of
// a given cached endpoint.
type endpointTrackerEntry struct {
// endpoint is the cached endpoint.
endpoint tailcfg.Endpoint
// until is the time until which this endpoint is being cached.
until time.Time
// index is the index within the containing endpointHeap.
index int
}
// endpointHeap is an ordered heap of endpointTrackerEntry structs, ordered in
// ascending order by the 'until' expiry time (i.e. oldest first).
type endpointHeap []*endpointTrackerEntry
var _ heap.Interface[*endpointTrackerEntry] = (*endpointHeap)(nil)
// Len implements heap.Interface.
func (eh endpointHeap) Len() int { return len(eh) }
// Less implements heap.Interface.
func (eh endpointHeap) Less(i, j int) bool {
// We want to store items so that the lowest item in the heap is the
// oldest, so that heap.Pop()-ing from the endpointHeap will remove the
// oldest entry.
return eh[i].until.Before(eh[j].until)
}
// Swap implements heap.Interface.
func (eh endpointHeap) Swap(i, j int) {
eh[i], eh[j] = eh[j], eh[i]
eh[i].index = i
eh[j].index = j
}
// Push implements heap.Interface.
func (eh *endpointHeap) Push(item *endpointTrackerEntry) {
n := len(*eh)
item.index = n
*eh = append(*eh, item)
}
// Pop implements heap.Interface.
func (eh *endpointHeap) Pop() *endpointTrackerEntry {
old := *eh
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*eh = old[0 : n-1]
return item
}
// Min returns a pointer to the minimum element in the heap, without removing
// it. Since this is a min-heap ordered by the 'until' field, this returns the
// chronologically "earliest" element in the heap.
//
// Len() must be non-zero.
func (eh endpointHeap) Min() *endpointTrackerEntry {
return eh[0]
}
// endpointTracker caches endpoints that are advertised to peers. This allows
// peers to still reach this node if there's a temporary endpoint flap; rather
// than withdrawing an endpoint and then re-advertising it the next time we run
// a netcheck, we keep advertising the endpoint until it's not present for a
// defined timeout.
//
// See tailscale/tailscale#7877 for more information.
type endpointTracker struct {
mu sync.Mutex
endpoints map[netip.Addr]*endpointHeap
}
// update takes as input the current sent of discovered endpoints and the
// current time, and returns the set of endpoints plus any previous-cached and
// non-expired endpoints that should be advertised to peers.
func (et *endpointTracker) update(now time.Time, eps []tailcfg.Endpoint) (epsPlusCached []tailcfg.Endpoint) {
var inputEps set.Slice[netip.AddrPort]
for _, ep := range eps {
inputEps.Add(ep.Addr)
}
et.mu.Lock()
defer et.mu.Unlock()
// Extend endpoints that already exist in the cache. We do this before
// we remove expired endpoints, below, so we don't remove something
// that would otherwise have survived by extending.
until := now.Add(endpointTrackerLifetime)
for _, ep := range eps {
et.extendLocked(ep, until)
}
// Now that we've extended existing endpoints, remove everything that
// has expired.
et.removeExpiredLocked(now)
// Add entries from the input set of endpoints into the cache; we do
// this after removing expired ones so that we can store as many as
// possible, with space freed by the entries removed after expiry.
for _, ep := range eps {
et.addLocked(now, ep, until)
}
// Finally, add entries to the return array that aren't already there.
epsPlusCached = eps
for _, heap := range et.endpoints {
for _, ep := range *heap {
// If the endpoint was in the input list, or has expired, skip it.
if inputEps.Contains(ep.endpoint.Addr) {
continue
} else if now.After(ep.until) {
// Defense-in-depth; should never happen since
// we removed expired entries above, but ignore
// it anyway.
continue
}
// We haven't seen this endpoint; add to the return array
epsPlusCached = append(epsPlusCached, ep.endpoint)
}
}
return epsPlusCached
}
// extendLocked will update the expiry time of the provided endpoint in the
// cache, if it is present. If it is not present, nothing will be done.
//
// et.mu must be held.
func (et *endpointTracker) extendLocked(ep tailcfg.Endpoint, until time.Time) {
key := ep.Addr.Addr()
epHeap, found := et.endpoints[key]
if !found {
return
}
// Find the entry for this exact address; this loop is quick since we
// bound the number of items in the heap.
//
// TODO(andrew): this means we iterate over the entire heap once per
// endpoint; even if the heap is small, if we have a lot of input
// endpoints this can be expensive?
for i, entry := range *epHeap {
if entry.endpoint == ep {
entry.until = until
heap.Fix(epHeap, i)
return
}
}
}
// addLocked will store the provided endpoint(s) in the cache for a fixed
// period of time, ensuring that the size of the endpoint cache remains below
// the maximum.
//
// et.mu must be held.
func (et *endpointTracker) addLocked(now time.Time, ep tailcfg.Endpoint, until time.Time) {
key := ep.Addr.Addr()
// Create or get the heap for this endpoint's addr
epHeap := et.endpoints[key]
if epHeap == nil {
epHeap = new(endpointHeap)
mak.Set(&et.endpoints, key, epHeap)
}
// Find the entry for this exact address; this loop is quick
// since we bound the number of items in the heap.
found := slices.ContainsFunc(*epHeap, func(v *endpointTrackerEntry) bool {
return v.endpoint == ep
})
if !found {
// Add address to heap; either the endpoint is new, or the heap
// was newly-created and thus empty.
heap.Push(epHeap, &endpointTrackerEntry{endpoint: ep, until: until})
}
// Now that we've added everything, pop from our heap until we're below
// the limit. This is a min-heap, so popping removes the lowest (and
// thus oldest) endpoint.
for epHeap.Len() > endpointTrackerMaxPerAddr {
heap.Pop(epHeap)
}
}
// removeExpired will remove all expired entries from the cache.
//
// et.mu must be held.
func (et *endpointTracker) removeExpiredLocked(now time.Time) {
for k, epHeap := range et.endpoints {
// The minimum element is oldest/earliest endpoint; repeatedly
// pop from the heap while it's in the past.
for epHeap.Len() > 0 {
minElem := epHeap.Min()
if now.After(minElem.until) {
heap.Pop(epHeap)
} else {
break
}
}
if epHeap.Len() == 0 {
// Free up space in the map by removing the empty heap.
delete(et.endpoints, k)
}
}
}

View File

@@ -1,187 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"net/netip"
"reflect"
"slices"
"strings"
"testing"
"time"
"tailscale.com/tailcfg"
)
func TestEndpointTracker(t *testing.T) {
local := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("192.168.1.1:12345"),
Type: tailcfg.EndpointLocal,
}
stun4_1 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("1.2.3.4:12345"),
Type: tailcfg.EndpointSTUN,
}
stun4_2 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("5.6.7.8:12345"),
Type: tailcfg.EndpointSTUN,
}
stun6_1 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("[2a09:8280:1::1111]:12345"),
Type: tailcfg.EndpointSTUN,
}
stun6_2 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("[2a09:8280:1::2222]:12345"),
Type: tailcfg.EndpointSTUN,
}
start := time.Unix(1681503440, 0)
steps := []struct {
name string
now time.Time
eps []tailcfg.Endpoint
want []tailcfg.Endpoint
}{
{
name: "initial endpoints",
now: start,
eps: []tailcfg.Endpoint{local, stun4_1, stun6_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "no change",
now: start.Add(1 * time.Minute),
eps: []tailcfg.Endpoint{local, stun4_1, stun6_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "missing stun4",
now: start.Add(2 * time.Minute),
eps: []tailcfg.Endpoint{local, stun6_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "missing stun6",
now: start.Add(3 * time.Minute),
eps: []tailcfg.Endpoint{local, stun4_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "multiple STUN addresses within timeout",
now: start.Add(4 * time.Minute),
eps: []tailcfg.Endpoint{local, stun4_2, stun6_2},
want: []tailcfg.Endpoint{local, stun4_1, stun4_2, stun6_1, stun6_2},
},
{
name: "endpoint extended",
now: start.Add(3*time.Minute + endpointTrackerLifetime - 1),
eps: []tailcfg.Endpoint{local},
want: []tailcfg.Endpoint{
local, stun4_2, stun6_2,
// stun4_1 had its lifetime extended by the
// "missing stun6" test above to that start
// time plus the lifetime, while stun6 should
// have expired a minute sooner. It should thus
// be in this returned list.
stun4_1,
},
},
{
name: "after timeout",
now: start.Add(4*time.Minute + endpointTrackerLifetime + 1),
eps: []tailcfg.Endpoint{local, stun4_2, stun6_2},
want: []tailcfg.Endpoint{local, stun4_2, stun6_2},
},
{
name: "after timeout still caches",
now: start.Add(4*time.Minute + endpointTrackerLifetime + time.Minute),
eps: []tailcfg.Endpoint{local},
want: []tailcfg.Endpoint{local, stun4_2, stun6_2},
},
}
var et endpointTracker
for _, tt := range steps {
t.Logf("STEP: %s", tt.name)
got := et.update(tt.now, tt.eps)
// Sort both arrays for comparison
slices.SortFunc(got, func(a, b tailcfg.Endpoint) int {
return strings.Compare(a.Addr.String(), b.Addr.String())
})
slices.SortFunc(tt.want, func(a, b tailcfg.Endpoint) int {
return strings.Compare(a.Addr.String(), b.Addr.String())
})
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("endpoints mismatch\ngot: %+v\nwant: %+v", got, tt.want)
}
}
}
func TestEndpointTrackerMaxNum(t *testing.T) {
start := time.Unix(1681503440, 0)
var allEndpoints []tailcfg.Endpoint // all created endpoints
mkEp := func(i int) tailcfg.Endpoint {
ep := tailcfg.Endpoint{
Addr: netip.AddrPortFrom(netip.MustParseAddr("1.2.3.4"), uint16(i)),
Type: tailcfg.EndpointSTUN,
}
allEndpoints = append(allEndpoints, ep)
return ep
}
var et endpointTracker
// Add more endpoints to the list than our limit
for i := 0; i <= endpointTrackerMaxPerAddr; i++ {
et.update(start.Add(time.Duration(i)*time.Second), []tailcfg.Endpoint{mkEp(10000 + i)})
}
// Now add two more, slightly later
got := et.update(start.Add(1*time.Minute), []tailcfg.Endpoint{
mkEp(10100),
mkEp(10101),
})
// We expect to get the last N endpoints per our per-Addr limit, since
// all of the endpoints have the same netip.Addr. The first endpoint(s)
// that we added were dropped because we had more than the limit for
// this Addr.
want := allEndpoints[len(allEndpoints)-endpointTrackerMaxPerAddr:]
compareEndpoints := func(got, want []tailcfg.Endpoint) {
t.Helper()
slices.SortFunc(want, func(a, b tailcfg.Endpoint) int {
return strings.Compare(a.Addr.String(), b.Addr.String())
})
slices.SortFunc(got, func(a, b tailcfg.Endpoint) int {
return strings.Compare(a.Addr.String(), b.Addr.String())
})
if !reflect.DeepEqual(got, want) {
t.Errorf("endpoints mismatch\ngot: %+v\nwant: %+v", got, want)
}
}
compareEndpoints(got, want)
// However, if we have more than our limit of endpoints passed in to
// the endpointTracker, we will return all of them (even if they're for
// the same address).
var inputEps []tailcfg.Endpoint
for i := 0; i < endpointTrackerMaxPerAddr+5; i++ {
inputEps = append(inputEps, tailcfg.Endpoint{
Addr: netip.AddrPortFrom(netip.MustParseAddr("1.2.3.4"), 10200+uint16(i)),
Type: tailcfg.EndpointSTUN,
})
}
want = inputEps
got = et.update(start.Add(2*time.Minute), inputEps)
compareEndpoints(got, want)
}

View File

@@ -53,6 +53,7 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/ringbuffer"
"tailscale.com/util/set"
"tailscale.com/util/uniq"
"tailscale.com/version"
"tailscale.com/wgengine/capture"
@@ -640,7 +641,6 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
if !c.setNearestDERP(ni.PreferredDERP) {
ni.PreferredDERP = 0
}
ni.FirewallMode = hostinfo.FirewallMode()
c.callNetInfoCallback(ni)
return report, nil
@@ -2594,6 +2594,11 @@ const (
// STUN-derived endpoint valid for. UDP NAT mappings typically
// expire at 30 seconds, so this is a few seconds shy of that.
endpointsFreshEnoughDuration = 27 * time.Second
// endpointTrackerLifetime is how long we continue advertising an
// endpoint after we last see it. This is intentionally chosen to be
// slightly longer than a full netcheck period.
endpointTrackerLifetime = 5*time.Minute + 10*time.Second
)
// Constants that are variable for testing.
@@ -2678,6 +2683,79 @@ type discoInfo struct {
lastPingTime time.Time
}
type endpointTrackerEntry struct {
endpoint tailcfg.Endpoint
until time.Time
}
type endpointTracker struct {
mu sync.Mutex
cache map[netip.AddrPort]endpointTrackerEntry
}
func (et *endpointTracker) update(now time.Time, eps []tailcfg.Endpoint) (epsPlusCached []tailcfg.Endpoint) {
epsPlusCached = eps
var inputEps set.Slice[netip.AddrPort]
for _, ep := range eps {
inputEps.Add(ep.Addr)
}
et.mu.Lock()
defer et.mu.Unlock()
// Add entries to the return array that aren't already there.
for k, ep := range et.cache {
// If the endpoint was in the input list, or has expired, skip it.
if inputEps.Contains(k) {
continue
} else if now.After(ep.until) {
continue
}
// We haven't seen this endpoint; add to the return array
epsPlusCached = append(epsPlusCached, ep.endpoint)
}
// Add entries from the original input array into the cache, and/or
// extend the lifetime of entries that are already in the cache.
until := now.Add(endpointTrackerLifetime)
for _, ep := range eps {
et.addLocked(now, ep, until)
}
// Remove everything that has now expired.
et.removeExpiredLocked(now)
return epsPlusCached
}
// add will store the provided endpoint(s) in the cache for a fixed period of
// time, and remove any entries in the cache that have expired.
//
// et.mu must be held.
func (et *endpointTracker) addLocked(now time.Time, ep tailcfg.Endpoint, until time.Time) {
// If we already have an entry for this endpoint, update the timeout on
// it; otherwise, add it.
entry, found := et.cache[ep.Addr]
if found {
entry.until = until
} else {
entry = endpointTrackerEntry{ep, until}
}
mak.Set(&et.cache, ep.Addr, entry)
}
// removeExpired will remove all expired entries from the cache
//
// et.mu must be held
func (et *endpointTracker) removeExpiredLocked(now time.Time) {
for k, ep := range et.cache {
if now.After(ep.until) {
delete(et.cache, k)
}
}
}
var (
metricNumPeers = clientmetric.NewGauge("magicsock_netmap_num_peers")
metricNumDERPConns = clientmetric.NewGauge("magicsock_num_derp_conns")

View File

@@ -18,6 +18,7 @@ import (
"net/http/httptest"
"net/netip"
"os"
"reflect"
"runtime"
"strconv"
"strings"
@@ -32,6 +33,7 @@ import (
"github.com/tailscale/wireguard-go/tun/tuntest"
"go4.org/mem"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
@@ -2339,6 +2341,116 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
}
}
func TestEndpointTracker(t *testing.T) {
local := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("192.168.1.1:12345"),
Type: tailcfg.EndpointLocal,
}
stun4_1 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("1.2.3.4:12345"),
Type: tailcfg.EndpointSTUN,
}
stun4_2 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("5.6.7.8:12345"),
Type: tailcfg.EndpointSTUN,
}
stun6_1 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("[2a09:8280:1::1111]:12345"),
Type: tailcfg.EndpointSTUN,
}
stun6_2 := tailcfg.Endpoint{
Addr: netip.MustParseAddrPort("[2a09:8280:1::2222]:12345"),
Type: tailcfg.EndpointSTUN,
}
start := time.Unix(1681503440, 0)
steps := []struct {
name string
now time.Time
eps []tailcfg.Endpoint
want []tailcfg.Endpoint
}{
{
name: "initial endpoints",
now: start,
eps: []tailcfg.Endpoint{local, stun4_1, stun6_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "no change",
now: start.Add(1 * time.Minute),
eps: []tailcfg.Endpoint{local, stun4_1, stun6_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "missing stun4",
now: start.Add(2 * time.Minute),
eps: []tailcfg.Endpoint{local, stun6_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "missing stun6",
now: start.Add(3 * time.Minute),
eps: []tailcfg.Endpoint{local, stun4_1},
want: []tailcfg.Endpoint{local, stun4_1, stun6_1},
},
{
name: "multiple STUN addresses within timeout",
now: start.Add(4 * time.Minute),
eps: []tailcfg.Endpoint{local, stun4_2, stun6_2},
want: []tailcfg.Endpoint{local, stun4_1, stun4_2, stun6_1, stun6_2},
},
{
name: "endpoint extended",
now: start.Add(3*time.Minute + endpointTrackerLifetime - 1),
eps: []tailcfg.Endpoint{local},
want: []tailcfg.Endpoint{
local, stun4_2, stun6_2,
// stun4_1 had its lifetime extended by the
// "missing stun6" test above to that start
// time plus the lifetime, while stun6 should
// have expired a minute sooner. It should thus
// be in this returned list.
stun4_1,
},
},
{
name: "after timeout",
now: start.Add(4*time.Minute + endpointTrackerLifetime + 1),
eps: []tailcfg.Endpoint{local, stun4_2, stun6_2},
want: []tailcfg.Endpoint{local, stun4_2, stun6_2},
},
{
name: "after timeout still caches",
now: start.Add(4*time.Minute + endpointTrackerLifetime + time.Minute),
eps: []tailcfg.Endpoint{local},
want: []tailcfg.Endpoint{local, stun4_2, stun6_2},
},
}
var et endpointTracker
for _, tt := range steps {
t.Logf("STEP: %s", tt.name)
got := et.update(tt.now, tt.eps)
// Sort both arrays for comparison
slices.SortFunc(got, func(a, b tailcfg.Endpoint) int {
return strings.Compare(a.Addr.String(), b.Addr.String())
})
slices.SortFunc(tt.want, func(a, b tailcfg.Endpoint) int {
return strings.Compare(a.Addr.String(), b.Addr.String())
})
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("endpoints mismatch\ngot: %+v\nwant: %+v", got, tt.want)
}
}
}
// applyNetworkMap is a test helper that sets the network map and
// configures WG.
func applyNetworkMap(t *testing.T, m *magicStack, nm *netmap.NetworkMap) {

View File

@@ -22,7 +22,6 @@ import (
"golang.org/x/sys/unix"
"golang.org/x/time/rate"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
@@ -98,36 +97,29 @@ func chooseFireWallMode(logf logger.Logf, det tableDetector) linuxfw.FirewallMod
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "nftables":
// TODO(KevinLiang10): Updates to a flag
logf("router: envknob TS_DEBUG_FIREWALL_MODE=nftables set")
hostinfo.SetFirewallMode("nft-forced")
return linuxfw.FirewallModeNfTables
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "iptables":
logf("router: envknob TS_DEBUG_FIREWALL_MODE=iptables set")
hostinfo.SetFirewallMode("ipt-forced")
return linuxfw.FirewallModeIPTables
case nftRuleCount > 0 && iptRuleCount == 0:
logf("router: nftables is currently in use")
hostinfo.SetFirewallMode("nft-inuse")
return linuxfw.FirewallModeNfTables
case iptRuleCount > 0 && nftRuleCount == 0:
logf("router: iptables is currently in use")
hostinfo.SetFirewallMode("ipt-inuse")
return linuxfw.FirewallModeIPTables
case nftAva:
// if both iptables and nftables are available but
// neither/both are currently used, use nftables.
logf("router: nftables is available")
hostinfo.SetFirewallMode("nft")
return linuxfw.FirewallModeNfTables
case iptAva:
logf("router: iptables is available")
hostinfo.SetFirewallMode("ipt")
return linuxfw.FirewallModeIPTables
default:
// if neither iptables nor nftables are available, use iptablesRunner as a dummy
// runner which exists but won't do anything. Creating iptablesRunner errors only
// if the iptables command is missing or doesnt support "--version", as long as it
// can determine a version then itll carry on.
hostinfo.SetFirewallMode("ipt-fb")
return linuxfw.FirewallModeIPTables
}
}