Compare commits
2 Commits
bradfitz/g
...
macsys-upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e230e617f8 | ||
|
|
a690347e62 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,7 +38,6 @@ cmd/tailscaled/tailscaled
|
||||
# Ignore web client node modules
|
||||
.vite/
|
||||
client/web/node_modules
|
||||
client/web/build
|
||||
|
||||
/gocross
|
||||
/dist
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.49.0
|
||||
1.47.0
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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, learn 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 doesn’t support "--version", as long as it
|
||||
// can determine a version then it’ll carry on.
|
||||
hostinfo.SetFirewallMode("ipt-fb")
|
||||
return linuxfw.FirewallModeIPTables
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user