summaryrefslogtreecommitdiffhomepage
path: root/tailcfg/tailcfg.go
blob: c704a62484620c948ac61d746182d445fc903e43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package tailcfg contains types used by the Tailscale protocol with between
// the node and the coordination server.
package tailcfg

//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,SSHPolicy --clonefunc

import (
	"bytes"
	"cmp"
	"encoding/json"
	"errors"
	"fmt"
	"maps"
	"net/netip"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"time"

	"tailscale.com/feature/buildfeatures"
	"tailscale.com/types/dnstype"
	"tailscale.com/types/key"
	"tailscale.com/types/opt"
	"tailscale.com/types/structs"
	"tailscale.com/types/tkatype"
	"tailscale.com/types/views"
	"tailscale.com/util/dnsname"
	"tailscale.com/util/slicesx"
	"tailscale.com/util/vizerror"
)

// CapabilityVersion represents the client's capability level. That
// is, it can be thought of as the client's simple version number: a
// single monotonically increasing integer, rather than the relatively
// complex x.y.z-xxxxx semver+hash(es). Whenever the client gains a
// capability or wants to negotiate a change in semantics with the
// server (control plane),  peers (over PeerAPI), or frontend (over
// LocalAPI), bump this number and document what's new.
//
// Previously (prior to 2022-03-06), it was known as the "MapRequest
// version" or "mapVer" or "map cap" and that name and usage persists
// in places.
type CapabilityVersion int

// CurrentCapabilityVersion is the current capability version of the codebase.
//
// History of versions:
//
//   - 3: implicit compression, keep-alives
//   - 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
//   - 5: 2020-10-19, implies IncludeIPv6, delta Peers/UserProfiles, supports MagicDNS
//   - 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged
//   - 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
//   - 8: 2020-12-19: client can buggily receive IPv6 addresses and routes if beta enabled server-side
//   - 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
//   - 10: 2021-01-17: client understands MapResponse.PeerSeenChange
//   - 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping
//   - 12: 2021-03-04: client understands PingRequest
//   - 13: 2021-03-19: client understands FilterRule.IPProto
//   - 14: 2021-04-07: client understands DNSConfig.Routes and DNSConfig.Resolvers
//   - 15: 2021-04-12: client treats nil MapResponse.DNSConfig as meaning unchanged
//   - 16: 2021-04-15: client understands Node.Online, MapResponse.OnlineChange
//   - 17: 2021-04-18: MapResponse.Domain empty means unchanged
//   - 18: 2021-04-19: MapResponse.Node nil means unchanged (all fields now omitempty)
//   - 19: 2021-04-21: MapResponse.Debug.SleepSeconds
//   - 20: 2021-06-11: MapResponse.LastSeen used even less (https://github.com/tailscale/tailscale/issues/2107)
//   - 21: 2021-06-15: added MapResponse.DNSConfig.CertDomains
//   - 22: 2021-06-16: added MapResponse.DNSConfig.ExtraRecords
//   - 23: 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
//   - 24: 2021-09-18: MapResponse.Health from control to node; node shows in "tailscale status"
//   - 25: 2021-11-01: MapResponse.Debug.Exit
//   - 26: 2022-01-12: (nothing, just bumping for 1.20.0)
//   - 27: 2022-02-18: start of SSHPolicy being respected
//   - 28: 2022-03-09: client can communicate over Noise.
//   - 29: 2022-03-21: MapResponse.PopBrowserURL
//   - 30: 2022-03-22: client can request id tokens.
//   - 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support
//   - 32: 2022-04-17: client knows FilterRule.CapMatch
//   - 33: 2022-07-20: added MapResponse.PeersChangedPatch (DERPRegion + Endpoints)
//   - 34: 2022-08-02: client understands CapabilityFileSharingTarget
//   - 36: 2022-08-02: added PeersChangedPatch.{Key,DiscoKey,Online,LastSeen,KeyExpiry,Capabilities}
//   - 37: 2022-08-09: added Debug.{SetForceBackgroundSTUN,SetRandomizeClientPort}; Debug are sticky
//   - 38: 2022-08-11: added PingRequest.URLIsNoise
//   - 39: 2022-08-15: clients can talk Noise over arbitrary HTTPS port
//   - 40: 2022-08-22: added Node.KeySignature, PeersChangedPatch.KeySignature
//   - 41: 2022-08-30: uses 100.100.100.100 for route-less ExtraRecords if global nameservers is set
//   - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
//   - 43: 2022-09-21: clients can return usernames for SSH
//   - 44: 2022-09-22: MapResponse.ControlDialPlan
//   - 45: 2022-09-26: c2n /debug/{goroutines,prefs,metrics}
//   - 46: 2022-10-04: c2n /debug/component-logging
//   - 47: 2022-10-11: Register{Request,Response}.NodeKeySignature
//   - 48: 2022-11-02: Node.UnsignedPeerAPIOnly
//   - 49: 2022-11-03: Client understands EarlyNoise
//   - 50: 2022-11-14: Client understands CapabilityIngress
//   - 51: 2022-11-30: Client understands CapabilityTailnetLockAlpha
//   - 52: 2023-01-05: client can handle c2n POST /logtail/flush
//   - 53: 2023-01-18: client respects explicit Node.Expired + auto-sets based on Node.KeyExpiry
//   - 54: 2023-01-19: Node.Cap added, PeersChangedPatch.Cap, uses Node.Cap for ExitDNS before Hostinfo.Services fallback
//   - 55: 2023-01-23: start of c2n GET+POST /update handler
//   - 56: 2023-01-24: Client understands CapabilityDebugTSDNSResolution
//   - 57: 2023-01-25: Client understands CapabilityBindToInterfaceByRoute
//   - 58: 2023-03-10: Client retries lite map updates before restarting map poll.
//   - 59: 2023-03-16: Client understands Peers[].SelfNodeV4MasqAddrForThisPeer
//   - 60: 2023-04-06: Client understands IsWireGuardOnly
//   - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
//   - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events
//   - 63: 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding.
//   - 64: 2023-07-11: Client understands s/CapabilityTailnetLockAlpha/CapabilityTailnetLock
//   - 65: 2023-07-12: Client understands DERPMap.HomeParams + incremental DERPMap updates with params
//   - 66: 2023-07-23: UserProfile.Groups added (available via WhoIs) (removed in 87)
//   - 67: 2023-07-25: Client understands PeerCapMap
//   - 68: 2023-08-09: Client has dedicated updateRoutine; MapRequest.Stream true means ignore Hostinfo+Endpoints
//   - 69: 2023-08-16: removed Debug.LogHeap* + GoroutineDumpURL; added c2n /debug/logheap
//   - 70: 2023-08-16: removed most Debug fields; added NodeAttrDisable*, NodeAttrDebug* instead
//   - 71: 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
//   - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
//   - 73: 2023-09-01: Non-Windows clients expect to receive ClientVersion
//   - 74: 2023-09-18: Client understands NodeCapMap
//   - 75: 2023-09-12: Client understands NodeAttrDNSForwarderDisableTCPRetries
//   - 76: 2023-09-20: Client understands ExitNodeDNSResolvers for IsWireGuardOnly nodes
//   - 77: 2023-10-03: Client understands Peers[].SelfNodeV6MasqAddrForThisPeer
//   - 78: 2023-10-05: can handle c2n Wake-on-LAN sending
//   - 79: 2023-10-05: Client understands UrgentSecurityUpdate in ClientVersion
//   - 80: 2023-11-16: can handle c2n GET /tls-cert-status
//   - 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates)
//   - 82: 2023-12-01: Client understands NodeAttrLinuxMustUseIPTables, NodeAttrLinuxMustUseNfTables, c2n /netfilter-kind
//   - 83: 2023-12-18: Client understands DefaultAutoUpdate
//   - 84: 2024-01-04: Client understands SeamlessKeyRenewal
//   - 85: 2024-01-05: Client understands MaxKeyDuration
//   - 86: 2024-01-23: Client understands NodeAttrProbeUDPLifetime
//   - 87: 2024-02-11: UserProfile.Groups removed (added in 66)
//   - 88: 2024-03-05: Client understands NodeAttrSuggestExitNode
//   - 89: 2024-03-23: Client no longer respects deleted PeerChange.Capabilities (use CapMap)
//   - 90: 2024-04-03: Client understands PeerCapabilityTaildrive.
//   - 91: 2024-04-24: Client understands PeerCapabilityTaildriveSharer.
//   - 92: 2024-05-06: Client understands NodeAttrUserDialUseRoutes.
//   - 93: 2024-05-06: added support for stateful firewalling.
//   - 94: 2024-05-06: Client understands Node.IsJailed.
//   - 95: 2024-05-06: Client uses NodeAttrUserDialUseRoutes to change DNS dialing behavior.
//   - 96: 2024-05-29: Client understands NodeAttrSSHBehaviorV1
//   - 97: 2024-06-06: Client understands NodeAttrDisableSplitDNSWhenNoCustomResolvers
//   - 98: 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
//   - 99: 2024-06-14: Client understands NodeAttrDisableLocalDNSOverrideViaNRPT
//   - 100: 2024-06-18: Initial support for filtertype.Match.SrcCaps - actually usable in capver 109 (issue #12542)
//   - 101: 2024-07-01: Client supports SSH agent forwarding when handling connections with /bin/su
//   - 102: 2024-07-12: NodeAttrDisableMagicSockCryptoRouting support
//   - 103: 2024-07-24: Client supports NodeAttrDisableCaptivePortalDetection
//   - 104: 2024-08-03: SelfNodeV6MasqAddrForThisPeer now works
//   - 105: 2024-08-05: Fixed SSH behavior on systems that use busybox (issue #12849)
//   - 106: 2024-09-03: fix panic regression from cryptokey routing change (65fe0ba7b5)
//   - 107: 2024-10-30: add App Connector to conffile (PR #13942)
//   - 108: 2024-11-08: Client sends ServicesHash in Hostinfo, understands c2n GET /vip-services.
//   - 109: 2024-11-18: Client supports filtertype.Match.SrcCaps (issue #12542)
//   - 110: 2024-12-12: removed never-before-used Tailscale SSH public key support (#14373)
//   - 111: 2025-01-14: Client supports a peer having Node.HomeDERP (issue #14636)
//   - 112: 2025-01-14: Client interprets AllowedIPs of nil as meaning same as Addresses
//   - 113: 2025-01-20: Client communicates to control whether funnel is enabled by sending Hostinfo.IngressEnabled (#14688)
//   - 114: 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code change) (#14829)
//   - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
//   - 116: 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
//   - 117: 2025-05-28: Client understands DisplayMessages (structured health messages), but not necessarily PrimaryAction.
//   - 118: 2025-07-01: Client sends Hostinfo.StateEncrypted to report whether the state file is encrypted at rest (#15830)
//   - 119: 2025-07-10: Client uses Hostinfo.Location.Priority to prioritize one route over another.
//   - 120: 2025-07-15: Client understands peer relay disco messages, and implements peer client and relay server functions
//   - 121: 2025-07-19: Client understands peer relay endpoint alloc with [disco.AllocateUDPRelayEndpointRequest] & [disco.AllocateUDPRelayEndpointResponse]
//   - 122: 2025-07-21: Client sends Hostinfo.ExitNodeID to report which exit node it has selected, if any.
//   - 123: 2025-07-28: fix deadlock regression from cryptokey routing change (issue #16651)
//   - 124: 2025-08-08: removed NodeAttrDisableMagicSockCryptoRouting support, crypto routing is now mandatory
//   - 125: 2025-08-11: dnstype.Resolver adds UseWithExitNode field.
//   - 126: 2025-09-17: Client uses seamless key renewal unless disabled by control (tailscale/corp#31479)
//   - 127: 2025-09-19: can handle C2N /debug/netmap.
//   - 128: 2025-10-02: can handle C2N /debug/health.
//   - 129: 2025-10-04: Fixed sleep/wake deadlock in magicsock when using peer relay (PR #17449)
//   - 130: 2025-10-06: client can send key.HardwareAttestationPublic and key.HardwareAttestationKeySignature in MapRequest
//   - 131: 2025-11-25: client respects [NodeAttrDefaultAutoUpdate]
//   - 132: 2026-02-13: client respects [NodeAttrDisableHostsFileUpdates]
//   - 133: 2026-02-17: client understands [NodeAttrForceRegisterMagicDNSIPv4Only]; MagicDNS IPv6 registered w/ OS by default
//   - 134: 2026-03-09: Client understands [NodeAttrDisableAndroidBindToActiveNetwork]
//   - 135: 2026-03-30: Client understands [NodeAttrCacheNetworkMaps]
//   - 136: 2026-04-09: Client understands [NodeAttrDisableLinuxCGNATDropRule]
const CurrentCapabilityVersion CapabilityVersion = 136

// ID is an integer ID for a user, node, or login allocated by the
// control plane.
//
// To be nice, control plane servers should not use int64s that are too large to
// fit in a JavaScript number (see JavaScript's Number.MAX_SAFE_INTEGER).
// The Tailscale-hosted control plane stopped allocating large integers in
// March 2023 but nodes prior to that may have IDs larger than
// MAX_SAFE_INTEGER (2^53 – 1).
//
// IDs must not be zero or negative.
type ID int64

// UserID is an [ID] for a [User].
type UserID ID

func (u UserID) IsZero() bool {
	return u == 0
}

// LoginID is an [ID] for a [Login].
//
// It is not used in the Tailscale client, but is used in the control plane.
type LoginID ID

func (u LoginID) IsZero() bool {
	return u == 0
}

// NodeID is a unique integer ID for a node.
//
// It's global within a control plane URL ("tailscale up --login-server") and is
// (as of 2025-01-06) never re-used even after a node is deleted.
//
// To be nice, control plane servers should not use int64s that are too large to
// fit in a JavaScript number (see JavaScript's Number.MAX_SAFE_INTEGER).
// The Tailscale-hosted control plane stopped allocating large integers in
// March 2023 but nodes prior to that may have node IDs larger than
// MAX_SAFE_INTEGER (2^53 – 1).
//
// NodeIDs are not stable across control plane URLs. For more stable URLs,
// see [StableNodeID].
type NodeID ID

func (u NodeID) IsZero() bool {
	return u == 0
}

// StableNodeID is a string form of [NodeID].
//
// Different control plane servers should ideally have different StableNodeID
// suffixes for different sites or regions.
//
// Being a string, it's safer to use in JavaScript without worrying about the
// size of the integer, as documented on [NodeID].
//
// But in general, Tailscale APIs can accept either a [NodeID] integer or a
// [StableNodeID] string when referring to a node.
type StableNodeID string

func (u StableNodeID) IsZero() bool {
	return u == ""
}

// User is a Tailscale user.
//
// A user can have multiple logins associated with it (e.g. gmail and github oauth).
// (Note: none of our UIs support this yet.)
//
// Some properties are inherited from the logins and can be overridden, such as
// display name and profile picture.
//
// Other properties must be the same for all logins associated with a user.
// In particular: domain. If a user has a "tailscale.io" domain login, they cannot
// have a general gmail address login associated with the user.
type User struct {
	ID            UserID
	DisplayName   string    // if non-empty overrides Login field
	ProfilePicURL string    `json:",omitzero"` // if non-empty overrides Login field
	Created       time.Time `json:",omitzero"`
}

// Login is a user from a specific identity provider, not associated with any
// particular tailnet.
type Login struct {
	_             structs.Incomparable
	ID            LoginID // unused in the Tailscale client
	Provider      string  // "google", "github", "okta_foo", etc.
	LoginName     string  // an email address or "email-ish" string (like alice@github)
	DisplayName   string  // from the IdP
	ProfilePicURL string  `json:",omitzero"` // from the IdP
}

// A UserProfile is display-friendly data for a [User].
// It includes the LoginName for display purposes but *not* the Provider.
// It also includes derived data from one of the user's logins.
type UserProfile struct {
	ID            UserID
	LoginName     string // "alice@smith.com"; for display purposes only (provider is not listed)
	DisplayName   string // "Alice Smith"
	ProfilePicURL string `json:",omitzero"`

	// Groups is a subset of SCIM groups (e.g. "engineering@example.com")
	// or group names in the tailnet policy document (e.g. "group:eng")
	// that contain this user and that the coordination server was
	// configured to report to this node.
	// The list is always sorted when loaded from storage.
	Groups []string `json:",omitempty"`
}

func (p *UserProfile) Equal(p2 *UserProfile) bool {
	if p == nil && p2 == nil {
		return true
	}
	if p == nil || p2 == nil {
		return false
	}
	return p.ID == p2.ID &&
		p.LoginName == p2.LoginName &&
		p.DisplayName == p2.DisplayName &&
		p.ProfilePicURL == p2.ProfilePicURL &&
		slices.Equal(p.Groups, p2.Groups)
}

// RawMessage is a raw encoded JSON value. It implements Marshaler and
// Unmarshaler and can be used to delay JSON decoding or precompute a JSON
// encoding.
//
// It is like json.RawMessage but is a string instead of a []byte to better
// portray immutable data.
type RawMessage string

// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
	if m == "" {
		return []byte("null"), nil
	}
	return []byte(m), nil
}

// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
	if m == nil {
		return errors.New("RawMessage: UnmarshalJSON on nil pointer")
	}
	*m = RawMessage(data)
	return nil
}

// MarshalCapJSON returns a capability rule in RawMessage string format.
func MarshalCapJSON[T any](capRule T) (RawMessage, error) {
	bs, err := json.Marshal(capRule)
	if err != nil {
		return "", fmt.Errorf("error marshalling capability rule: %w", err)
	}
	return RawMessage(string(bs)), nil
}

// Node is a Tailscale device in a tailnet.
type Node struct {
	ID       NodeID
	StableID StableNodeID

	// Name is the FQDN of the node.
	// It is also the MagicDNS name for the node.
	// It has a trailing dot.
	// e.g. "host.tail-scale.ts.net."
	Name string

	// User is the user who created the node. If ACL tags are in use for the
	// node then it doesn't reflect the ACL identity that the node is running
	// as.
	User UserID

	// Sharer, if non-zero, is the user who shared this node, if different than User.
	Sharer UserID `json:",omitzero"`

	Key          key.NodePublic
	KeyExpiry    time.Time                  `json:",omitzero"` // the zero value if this node does not expire
	KeySignature tkatype.MarshaledSignature `json:",omitempty"`
	Machine      key.MachinePublic          `json:",omitzero"`
	DiscoKey     key.DiscoPublic            `json:",omitzero"`

	// Addresses are the IP addresses of this Node directly.
	Addresses []netip.Prefix

	// AllowedIPs are the IP ranges to route to this node.
	//
	// As of CapabilityVersion 112, this may be nil (null or undefined) on the wire
	// to mean the same as Addresses. Internally, it is always filled in with
	// its possibly-implicit value.
	AllowedIPs []netip.Prefix `json:",omitzero"` // _not_ omitempty; only nil is special

	Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)

	// LegacyDERPString is this node's home LegacyDERPString region ID integer, but shoved into an
	// IP:port string for legacy reasons. The IP address is always "127.3.3.40"
	// (a loopback address (127) followed by the digits over the letters DERP on
	// a QWERTY keyboard (3.3.40)). The "port number" is the home LegacyDERPString region ID
	// integer.
	//
	// Deprecated: HomeDERP has replaced this, but old servers might still send
	// this field. See tailscale/tailscale#14636. Do not use this field in code
	// other than in the upgradeNode func, which canonicalizes it to HomeDERP
	// if it arrives as a LegacyDERPString string on the wire.
	LegacyDERPString string `json:"DERP,omitzero"` // DERP-in-IP:port ("127.3.3.40:N") endpoint

	// HomeDERP is the modern version of the DERP string field, with just an
	// integer. The client advertises support for this as of capver 111.
	//
	// HomeDERP may be zero if not (yet) known, but ideally always be non-zero
	// for magicsock connectivity to function normally.
	HomeDERP int `json:",omitzero"` // DERP region ID of the node's home DERP

	Hostinfo HostinfoView      `json:",omitzero"`
	Created  time.Time         `json:",omitzero"`
	Cap      CapabilityVersion `json:",omitzero"` // if non-zero, the node's capability version; old servers might not send

	// Tags are the list of ACL tags applied to this node.
	// Tags take the form of `tag:<value>` where value starts
	// with a letter and only contains alphanumerics and dashes `-`.
	// Some valid tag examples:
	//   `tag:prod`
	//   `tag:database`
	//   `tag:lab-1`
	Tags []string `json:",omitempty"`

	// PrimaryRoutes are the routes from AllowedIPs that this node
	// is currently the primary subnet router for, as determined
	// by the control plane. It does not include the self address
	// values from Addresses that are in AllowedIPs.
	PrimaryRoutes []netip.Prefix `json:",omitempty"`

	// LastSeen is when the node was last online. It is not
	// updated when Online is true. It is nil if the current
	// node doesn't have permission to know, or the node
	// has never been online.
	LastSeen *time.Time `json:",omitempty"`

	// Online is whether the node is currently connected to the
	// coordination server.  A value of nil means unknown, or the
	// current node doesn't have permission to know.
	Online *bool `json:",omitempty"`

	MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus

	// Capabilities are capabilities that the node has.
	// They're free-form strings, but should be in the form of URLs/URIs
	// such as:
	//    "https://tailscale.com/cap/is-admin"
	//    "https://tailscale.com/cap/file-sharing"
	//
	// Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508
	Capabilities []NodeCapability `json:",omitempty"`

	// CapMap is a map of capabilities to their optional argument/data values.
	//
	// It is valid for a capability to not have any argument/data values; such
	// capabilities can be tested for using the HasCap method. These type of
	// capabilities are used to indicate that a node has a capability, but there
	// is no additional data associated with it. These were previously
	// represented by the Capabilities field, but can now be represented by
	// CapMap with an empty value.
	//
	// See NodeCapability for more information on keys.
	//
	// Metadata about nodes can be transmitted in 3 ways:
	// 1. MapResponse.Node.CapMap describes attributes that affect behavior for
	//    this node, such as which features have been enabled through the admin
	//    panel and any associated configuration details.
	// 2. MapResponse.PacketFilter(s) describes access (both IP and application
	//    based) that should be granted to peers.
	// 3. MapResponse.Peers[].CapMap describes attributes regarding a peer node,
	//    such as which features the peer supports or if that peer is preferred
	//    for a particular task vs other peers that could also be chosen.
	CapMap NodeCapMap `json:",omitempty"`

	// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
	// restrictions. However, in exchange for that privilege, it does not get
	// network access. It can only access this node's peerapi, which may not let
	// it do anything. It is the tailscaled client's job to double-check the
	// MapResponse's PacketFilter to verify that its AllowedIPs will not be
	// accepted by the packet filter.
	UnsignedPeerAPIOnly bool `json:",omitzero"`

	// The following three computed fields hold the various names that can
	// be used for this node in UIs. They are populated from controlclient
	// (not from control) by calling node.InitDisplayNames. These can be
	// used directly or accessed via node.DisplayName or node.DisplayNames.

	ComputedName            string `json:",omitzero"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
	computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
	ComputedNameWithHost    string `json:",omitzero"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set

	// DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging.
	DataPlaneAuditLogID string `json:",omitzero"`

	// Expired is whether this node's key has expired. Control may send
	// this; clients are only allowed to set this from false to true. On
	// the client, this is calculated client-side based on a timestamp sent
	// from control, to avoid clock skew issues.
	Expired bool `json:",omitzero"`

	// SelfNodeV4MasqAddrForThisPeer is the IPv4 that this peer knows the current node as.
	// It may be empty if the peer knows the current node by its native
	// IPv4 address.
	// This field is only populated in a MapResponse for peers and not
	// for the current node.
	//
	// If set, it should be used to masquerade traffic originating from the
	// current node to this peer. The masquerade address is only relevant
	// for this peer and not for other peers.
	//
	// This only applies to traffic originating from the current node to the
	// peer or any of its subnets. Traffic originating from subnet routes will
	// not be masqueraded (e.g. in case of --snat-subnet-routes).
	SelfNodeV4MasqAddrForThisPeer *netip.Addr `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978

	// SelfNodeV6MasqAddrForThisPeer is the IPv6 that this peer knows the current node as.
	// It may be empty if the peer knows the current node by its native
	// IPv6 address.
	// This field is only populated in a MapResponse for peers and not
	// for the current node.
	//
	// If set, it should be used to masquerade traffic originating from the
	// current node to this peer. The masquerade address is only relevant
	// for this peer and not for other peers.
	//
	// This only applies to traffic originating from the current node to the
	// peer or any of its subnets. Traffic originating from subnet routes will
	// not be masqueraded (e.g. in case of --snat-subnet-routes).
	SelfNodeV6MasqAddrForThisPeer *netip.Addr `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978

	// IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it
	// is not expected to speak Disco or DERP, and it must have Endpoints in
	// order to be reachable.
	IsWireGuardOnly bool `json:",omitzero"`

	// IsJailed indicates that this node is jailed and should not be allowed
	// initiate connections, however outbound connections to it should still be
	// allowed.
	IsJailed bool `json:",omitzero"`

	// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
	// node is marked IsWireGuardOnly and being used as an exit node.
	ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"`
}

// HasCap reports whether the node has the given capability.
// It is safe to call on an invalid NodeView.
func (v NodeView) HasCap(cap NodeCapability) bool {
	return v.ж.HasCap(cap)
}

// HasCap reports whether the node has the given capability.
// It is safe to call on a nil Node.
func (v *Node) HasCap(cap NodeCapability) bool {
	return v != nil && v.CapMap.Contains(cap)
}

// DisplayName returns the user-facing name for a node which should
// be shown in client UIs.
//
// Parameter forOwner specifies whether the name is requested by
// the owner of the node. When forOwner is false, the hostname is
// never included in the return value.
//
// Return value is either "Name" or "Name (Hostname)", where
// Name is the node's MagicDNS base name (for normal non-shared-in
// nodes), FQDN (without trailing dot, for shared-in nodes), or
// Hostname (if no MagicDNS). Hostname is only included in the
// return value if it varies from Name and forOwner is provided true.
//
// DisplayName is only valid if InitDisplayNames has been called.
func (n *Node) DisplayName(forOwner bool) string {
	if forOwner {
		return n.ComputedNameWithHost
	}
	return n.ComputedName
}

// DisplayName returns the decomposed user-facing name for a node.
//
// Parameter forOwner specifies whether the name is requested by
// the owner of the node. When forOwner is false, hostIfDifferent
// is always returned empty.
//
// Return value name is the node's primary name, populated with the
// node's MagicDNS base name (for normal non-shared-in nodes), FQDN
// (without trailing dot, for shared-in nodes), or Hostname (if no
// MagicDNS).
//
// Return value hostIfDifferent, when non-empty, is the node's
// hostname. hostIfDifferent is only populated when the hostname
// varies from name and forOwner is provided as true.
//
// DisplayNames is only valid if InitDisplayNames has been called.
func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
	if forOwner {
		return n.ComputedName, n.computedHostIfDifferent
	}
	return n.ComputedName, ""
}

// IsTagged reports whether the node has any tags.
func (n *Node) IsTagged() bool {
	return len(n.Tags) > 0
}

// SharerOrUser Sharer if set, else User.
func (n *Node) SharerOrUser() UserID {
	return cmp.Or(n.Sharer, n.User)
}

// IsTagged reports whether the node has any tags.
func (n NodeView) IsTagged() bool { return n.ж.IsTagged() }

// DisplayName wraps Node.DisplayName.
func (n NodeView) DisplayName(forOwner bool) string { return n.ж.DisplayName(forOwner) }

// SharerOrUser wraps Node.SharerOrUser.
func (n NodeView) SharerOrUser() UserID { return n.ж.SharerOrUser() }

// InitDisplayNames computes and populates n's display name
// fields: n.ComputedName, n.computedHostIfDifferent, and
// n.ComputedNameWithHost.
func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
	name := dnsname.TrimSuffix(n.Name, networkMagicDNSSuffix)
	var hostIfDifferent string
	if n.Hostinfo.Valid() {
		hostIfDifferent = dnsname.SanitizeHostname(n.Hostinfo.Hostname())
	}

	if strings.EqualFold(name, hostIfDifferent) {
		hostIfDifferent = ""
	}
	if name == "" {
		if hostIfDifferent != "" {
			name = hostIfDifferent
			hostIfDifferent = ""
		} else {
			name = n.Key.String()
		}
	}

	var nameWithHost string
	if hostIfDifferent != "" {
		nameWithHost = fmt.Sprintf("%s (%s)", name, hostIfDifferent)
	} else {
		nameWithHost = name
	}

	n.ComputedName = name
	n.computedHostIfDifferent = hostIfDifferent
	n.ComputedNameWithHost = nameWithHost
}

// MachineStatus is the state of a [Node]'s approval into a tailnet.
//
// A "node" and a "machine" are often 1:1, but technically a Tailscale
// daemon has one machine key and can have multiple nodes (e.g. different
// users on Windows) for that one machine key.
type MachineStatus int

const (
	MachineUnknown      = MachineStatus(iota)
	MachineUnauthorized // server has yet to approve
	MachineAuthorized   // server has approved
	MachineInvalid      // server has explicitly rejected this machine key
)

func (m MachineStatus) AppendText(b []byte) ([]byte, error) {
	return append(b, m.String()...), nil
}

func (m MachineStatus) MarshalText() ([]byte, error) {
	return []byte(m.String()), nil
}

func (m *MachineStatus) UnmarshalText(b []byte) error {
	switch string(b) {
	case "machine-unknown":
		*m = MachineUnknown
	case "machine-unauthorized":
		*m = MachineUnauthorized
	case "machine-authorized":
		*m = MachineAuthorized
	case "machine-invalid":
		*m = MachineInvalid
	default:
		var val int
		if _, err := fmt.Sscanf(string(b), "machine-unknown(%d)", &val); err != nil {
			*m = MachineStatus(val)
		} else {
			*m = MachineUnknown
		}
	}
	return nil
}

func (m MachineStatus) String() string {
	switch m {
	case MachineUnknown:
		return "machine-unknown"
	case MachineUnauthorized:
		return "machine-unauthorized"
	case MachineAuthorized:
		return "machine-authorized"
	case MachineInvalid:
		return "machine-invalid"
	default:
		return fmt.Sprintf("machine-unknown(%d)", int(m))
	}
}

func isNum(b byte) bool {
	return b >= '0' && b <= '9'
}

func isAlpha(b byte) bool {
	return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
}

// CheckTag validates tag for use as an ACL tag.
// For now we allow only ascii alphanumeric tags, and they need to start
// with a letter. No unicode shenanigans allowed, and we reserve punctuation
// marks other than '-' for a possible future URI scheme.
//
// Because we're ignoring unicode entirely, we can treat utf-8 as a series of
// bytes. Anything >= 128 is disqualified anyway.
//
// We might relax these rules later.
func CheckTag(tag string) error {
	var ok bool
	tag, ok = strings.CutPrefix(tag, "tag:")
	if !ok {
		return errors.New("tags must start with 'tag:'")
	}
	if tag == "" {
		return errors.New("tag names must not be empty")
	}
	if !isAlpha(tag[0]) {
		return errors.New("tag names must start with a letter, after 'tag:'")
	}

	for _, b := range []byte(tag) {
		if !isNum(b) && !isAlpha(b) && b != '-' {
			return errors.New("tag names can only contain numbers, letters, or dashes")
		}
	}

	return nil
}

// CheckRequestTags checks that all of h.RequestTags are valid.
func (h *Hostinfo) CheckRequestTags() error {
	if h == nil {
		return nil
	}
	for _, tag := range h.RequestTags {
		if err := CheckTag(tag); err != nil {
			return fmt.Errorf("tag(%#v): %w", tag, err)
		}
	}
	return nil
}

// ServiceProto is a service type. It's usually
// TCP ("tcp") or UDP ("udp"), but it can also have
// meta service values as defined in Service.Proto.
type ServiceProto string

const (
	TCP        = ServiceProto("tcp")
	UDP        = ServiceProto("udp")
	PeerAPI4   = ServiceProto("peerapi4")
	PeerAPI6   = ServiceProto("peerapi6")
	PeerAPIDNS = ServiceProto("peerapi-dns-proxy")
)

// IsKnownServiceProto checks whether sp represents a known-valid value of
// ServiceProto.
func IsKnownServiceProto(sp ServiceProto) bool {
	switch sp {
	case TCP, UDP, PeerAPI4, PeerAPI6, PeerAPIDNS, ServiceProto("egg"):
		return true
	}
	return false
}

// Service represents a service running on a node.
type Service struct {
	_ structs.Incomparable

	// Proto is the type of service. It's usually the constant TCP
	// or UDP ("tcp" or "udp"), but it can also be one of the
	// following meta service values:
	//
	//     * "peerapi4": peerapi is available on IPv4; Port is the
	//        port number that the peerapi is running on the
	//        node's Tailscale IPv4 address.
	//     * "peerapi6": peerapi is available on IPv6; Port is the
	//        port number that the peerapi is running on the
	//        node's Tailscale IPv6 address.
	//     * "peerapi-dns-proxy": the local peerapi service supports
	//        being a DNS proxy (when the node is an exit
	//        node). For this service, the Port number must only be 1.
	Proto ServiceProto

	// Port is the port number.
	//
	// For Proto "peerapi-dns", it must be 1.
	Port uint16

	// Description is the textual description of the service,
	// usually the process name that's running.
	Description string `json:",omitempty"`

	// TODO(apenwarr): allow advertising services on subnet IPs?
	// TODO(apenwarr): add "tags" here for each service?
}

// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
// explicitly declared by a node.
type Location struct {
	Country     string `json:",omitempty"` // User friendly country name, with proper capitalization ("Canada")
	CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in upper case ("CA")
	City        string `json:",omitempty"` // User friendly city name, with proper capitalization ("Squamish")

	// CityCode is a short code representing the city in upper case.
	// CityCode is used to disambiguate a city from another location
	// with the same city name. It uniquely identifies a particular
	// geographical location, within the tailnet.
	// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE")
	CityCode string `json:",omitempty"`

	// Latitude, Longitude are optional geographical coordinates of the node, in degrees.
	// No particular accuracy level is promised; the coordinates may simply be the center of the city or country.
	Latitude  float64 `json:",omitempty"`
	Longitude float64 `json:",omitempty"`

	// Priority determines the order of use of an exit node when a
	// location based preference matches more than one exit node,
	// the node with the highest priority wins. Nodes of equal
	// probability may be selected arbitrarily.
	//
	// A value of 0 means the exit node does not have a priority
	// preference. A negative int is not allowed.
	Priority int `json:",omitempty"`
}

// Hostinfo contains a summary of a Tailscale host.
//
// Because it contains pointers (slices), this type should not be used
// as a value type.
type Hostinfo struct {
	IPNVersion    string `json:",omitzero"` // version of this code (in version.Long format)
	FrontendLogID string `json:",omitzero"` // logtail ID of frontend instance
	BackendLogID  string `json:",omitzero"` // logtail ID of backend instance
	OS            string `json:",omitzero"` // operating system the client runs on (a version.OS value)

	// OSVersion is the version of the OS, if available.
	//
	// For Android, it's like "10", "11", "12", etc. For iOS and macOS it's like
	// "15.6.1" or "12.4.0". For Windows it's like "10.0.19044.1889". For
	// FreeBSD it's like "12.3-STABLE".
	//
	// For Linux, prior to Tailscale 1.32, we jammed a bunch of fields into this
	// string on Linux, like "Debian 10.4; kernel=xxx; container; env=kn" and so
	// on. As of Tailscale 1.32, this is simply the kernel version on Linux, like
	// "5.10.0-17-amd64".
	OSVersion string `json:",omitzero"`

	Container      opt.Bool `json:",omitzero"` // best-effort whether the client is running in a container
	Env            string   `json:",omitzero"` // a hostinfo.EnvType in string form
	Distro         string   `json:",omitzero"` // "debian", "ubuntu", "nixos", ...
	DistroVersion  string   `json:",omitzero"` // "20.04", ...
	DistroCodeName string   `json:",omitzero"` // "jammy", "bullseye", ...

	// App is used to disambiguate Tailscale clients that run using tsnet.
	App string `json:",omitzero"` // "k8s-operator", "golinks", ...

	Desktop         opt.Bool `json:",omitzero"` // if a desktop was detected on Linux
	Package         string   `json:",omitzero"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
	DeviceModel     string   `json:",omitzero"` // mobile phone model ("Pixel 3a", "iPhone12,3")
	PushDeviceToken string   `json:",omitzero"` // macOS/iOS APNs device token for notifications (and Android in the future)
	Hostname        string   `json:",omitzero"` // name of the host the client runs on
	ShieldsUp       bool     `json:",omitzero"` // indicates whether the host is blocking incoming connections
	ShareeNode      bool     `json:",omitzero"` // indicates this node exists in netmap because it's owned by a shared-to user
	NoLogsNoSupport bool     `json:",omitzero"` // indicates that the user has opted out of sending logs and support
	// WireIngress indicates that the node would like to be wired up server-side
	// (DNS, etc) to be able to use Tailscale Funnel, even if it's not currently
	// enabled. For example, the user might only use it for intermittent
	// foreground CLI serve sessions, for which they'd like it to work right
	// away, even if it's disabled most of the time. As an optimization, this is
	// only sent if IngressEnabled is false, as IngressEnabled implies that this
	// option is true.
	WireIngress     bool           `json:",omitzero"`
	IngressEnabled  bool           `json:",omitzero"`  // if the node has any funnel endpoint enabled
	AllowsUpdate    bool           `json:",omitzero"`  // indicates that the node has opted-in to admin-console-drive remote updates
	Machine         string         `json:",omitzero"`  // the current host's machine type (uname -m)
	GoArch          string         `json:",omitzero"`  // GOARCH value (of the built binary)
	GoArchVar       string         `json:",omitzero"`  // GOARM, GOAMD64, etc (of the built binary)
	GoVersion       string         `json:",omitzero"`  // Go version binary was built with
	RoutableIPs     []netip.Prefix `json:",omitempty"` // set of IP ranges this client can route
	RequestTags     []string       `json:",omitempty"` // set of ACL tags this node wants to claim
	WoLMACs         []string       `json:",omitempty"` // MAC address(es) to send Wake-on-LAN packets to wake this node (lowercase hex w/ colons)
	Services        []Service      `json:",omitempty"` // services advertised by this machine
	NetInfo         *NetInfo       `json:",omitzero"`
	SSH_HostKeys    []string       `json:"sshHostKeys,omitempty"` // if advertised
	Cloud           string         `json:",omitzero"`
	Userspace       opt.Bool       `json:",omitzero"` // if the client is running in userspace (netstack) mode
	UserspaceRouter opt.Bool       `json:",omitzero"` // if the client's subnet router is running in userspace (netstack) mode
	AppConnector    opt.Bool       `json:",omitzero"` // if the client is running the app-connector service
	ServicesHash    string         `json:",omitzero"` // opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n
	PeerRelay       bool           `json:",omitzero"` // if the client is willing to relay traffic for other peers
	ExitNodeID      StableNodeID   `json:",omitzero"` // the client’s selected exit node, empty when unselected.

	// Location represents geographical location data about a
	// Tailscale host. Location is optional and only set if
	// explicitly declared by a node.
	Location *Location `json:",omitzero"`

	TPM *TPMInfo `json:",omitzero"` // TPM device metadata, if available
	// StateEncrypted reports whether the node state is stored encrypted on
	// disk. The actual mechanism is platform-specific:
	//   * Apple nodes use the Keychain
	//   * Linux and Windows nodes use the TPM
	//   * Android apps use EncryptedSharedPreferences
	StateEncrypted opt.Bool `json:",omitzero"`

	// NOTE: any new fields containing pointers in this type
	//       require changes to Hostinfo.Equal.
}

// TPMInfo contains information about a TPM 2.0 device present on a node.
// All fields are read from TPM_CAP_TPM_PROPERTIES, see Part 2, section 6.13 of
// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
type TPMInfo struct {
	// Manufacturer is a 4-letter code from section 4.1 of
	// https://trustedcomputinggroup.org/resource/vendor-id-registry/,
	// for example "MSFT" for Microsoft.
	// Read from TPM_PT_MANUFACTURER.
	Manufacturer string `json:",omitzero"`
	// Vendor is a vendor ID string, up to 16 characters.
	// Read from TPM_PT_VENDOR_STRING_*.
	Vendor string `json:",omitzero"`
	// Model is a vendor-defined TPM model.
	// Read from TPM_PT_VENDOR_TPM_TYPE.
	Model int `json:",omitzero"`
	// FirmwareVersion is the version number of the firmware.
	// Read from TPM_PT_FIRMWARE_VERSION_*.
	FirmwareVersion uint64 `json:",omitzero"`
	// SpecRevision is the TPM 2.0 spec revision encoded as a single number. All
	// revisions can be found at
	// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
	// Before revision 184, TCG used the "01.83" format for revision 183.
	SpecRevision int `json:",omitzero"`

	// FamilyIndicator is the TPM spec family, like "2.0".
	// Read from TPM_PT_FAMILY_INDICATOR.
	FamilyIndicator string `json:",omitzero"`
}

// Present reports whether a TPM device is present on this machine.
func (t *TPMInfo) Present() bool { return t != nil }

// ServiceName is the name of a service, of the form `svc:dns-label`. Services
// represent some kind of application provided for users of the tailnet with a
// MagicDNS name and possibly dedicated IP addresses. Currently (2024-01-21),
// the only type of service is [VIPService].
// This is not related to the older [Service] used in [Hostinfo.Services].
type ServiceName string

// AsServiceName reports whether the given string is a valid service name.
// If so returns the name as a [tailcfg.ServiceName], otherwise returns "".
func AsServiceName(s string) ServiceName {
	svcName := ServiceName(s)
	if err := svcName.Validate(); err != nil {
		return ""
	}
	return svcName
}

// Validate validates if the service name is formatted correctly.
// We only allow valid DNS labels, since the expectation is that these will be
// used as parts of domain names. All errors are [vizerror.Error].
func (sn ServiceName) Validate() error {
	bareName, ok := strings.CutPrefix(string(sn), "svc:")
	if !ok {
		return vizerror.Errorf("%q is not a valid service name: must start with 'svc:'", sn)
	}
	if bareName == "" {
		return vizerror.Errorf("%q is not a valid service name: must not be empty after the 'svc:' prefix", sn)
	}
	return dnsname.ValidLabel(bareName)
}

// String implements [fmt.Stringer].
func (sn ServiceName) String() string {
	return string(sn)
}

// WithoutPrefix is the name of the service without the `svc:` prefix, used for
// DNS names. If the name does not include the prefix (which means
// [ServiceName.Validate] would return an error) then it returns "".
func (sn ServiceName) WithoutPrefix() string {
	bareName, ok := strings.CutPrefix(string(sn), "svc:")
	if !ok {
		return ""
	}
	return bareName
}

// VIPService represents a service created on a tailnet from the
// perspective of a node providing that service. These services
// have an virtual IP (VIP) address pair distinct from the node's IPs.
type VIPService struct {
	// Name is the name of the service. The Name uniquely identifies a service
	// on a particular tailnet, and so also corresponds uniquely to the pair of
	// IP addresses belonging to the VIP service.
	Name ServiceName

	// Ports specify which ProtoPorts are made available by this node
	// on the service's IPs.
	Ports []ProtoPortRange

	// Active specifies whether new requests for the service should be
	// sent to this node by control.
	Active bool
}

// TailscaleSSHEnabled reports whether or not this node is acting as a
// Tailscale SSH server.
func (hi *Hostinfo) TailscaleSSHEnabled() bool {
	// Currently, we use `SSH_HostKeys` as a proxy for this. However, we may later
	// include non-Tailscale host keys, and will add a separate flag to rely on.
	return hi != nil && len(hi.SSH_HostKeys) > 0
}

func (v HostinfoView) TailscaleSSHEnabled() bool { return v.ж.TailscaleSSHEnabled() }

// NetInfo contains information about the host's network state.
type NetInfo struct {
	// MappingVariesByDestIP says whether the host's NAT mappings
	// vary based on the destination IP.
	MappingVariesByDestIP opt.Bool `json:",omitzero"`

	// WorkingIPv6 is whether the host has IPv6 internet connectivity.
	WorkingIPv6 opt.Bool `json:",omitzero"`

	// OSHasIPv6 is whether the OS supports IPv6 at all, regardless of
	// whether IPv6 internet connectivity is available.
	OSHasIPv6 opt.Bool `json:",omitzero"`

	// WorkingUDP is whether the host has UDP internet connectivity.
	WorkingUDP opt.Bool `json:",omitzero"`

	// WorkingICMPv4 is whether ICMPv4 works.
	// Empty means not checked.
	WorkingICMPv4 opt.Bool `json:",omitzero"`

	// HavePortMap is whether we have an existing portmap open
	// (UPnP, PMP, or PCP).
	HavePortMap bool `json:",omitzero"`

	// UPnP is whether UPnP appears present on the LAN.
	// Empty means not checked.
	UPnP opt.Bool `json:",omitzero"`

	// PMP is whether NAT-PMP appears present on the LAN.
	// Empty means not checked.
	PMP opt.Bool `json:",omitzero"`

	// PCP is whether PCP appears present on the LAN.
	// Empty means not checked.
	PCP opt.Bool `json:",omitzero"`

	// PreferredDERP is this node's preferred (home) DERP region ID.
	// This is where the node expects to be contacted to begin a
	// peer-to-peer connection. The node might be be temporarily
	// connected to multiple DERP servers (to speak to other nodes
	// that are located elsewhere) but PreferredDERP is the region ID
	// that the node subscribes to traffic at.
	// Zero means disconnected or unknown.
	PreferredDERP int `json:",omitzero"`

	// LinkType is the current link type, if known.
	LinkType string `json:",omitzero"` // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)

	// DERPLatency is the fastest recent time to reach various
	// DERP STUN servers, in seconds. The map key is the
	// "regionID-v4" or "-v6"; it was previously the DERP server's
	// STUN host:port.
	//
	// This should only be updated rarely, or when there's a
	// material change, as any change here also gets uploaded to
	// the control plane.
	DERPLatency map[string]float64 `json:",omitempty"`

	// FirewallMode encodes both which firewall mode was selected and why.
	// It is Linux-specific (at least as of 2023-08-19) and is meant to help
	// debug iptables-vs-nftables issues. The string is of the form
	// "{nft,ift}-REASON", like "nft-forced" or "ipt-default". Empty means
	// either not Linux or a configuration in which the host firewall rules
	// are not managed by tailscaled.
	FirewallMode string `json:",omitzero"`

	// Update BasicallyEqual when adding fields.
}

func (ni *NetInfo) String() string {
	if ni == nil {
		return "NetInfo(nil)"
	}
	return fmt.Sprintf("NetInfo{varies=%v ipv6=%v ipv6os=%v udp=%v icmpv4=%v derp=#%v portmap=%v link=%q firewallmode=%q}",
		ni.MappingVariesByDestIP, ni.WorkingIPv6,
		ni.OSHasIPv6, ni.WorkingUDP, ni.WorkingICMPv4,
		ni.PreferredDERP, ni.portMapSummary(), ni.LinkType, ni.FirewallMode)
}

func (ni *NetInfo) portMapSummary() string {
	if !buildfeatures.HasPortMapper {
		return "x"
	}
	if !ni.HavePortMap && ni.UPnP == "" && ni.PMP == "" && ni.PCP == "" {
		return "?"
	}
	var prefix string
	if ni.HavePortMap {
		prefix = "active-"
	}
	return prefix + conciseOptBool(ni.UPnP, "U") + conciseOptBool(ni.PMP, "M") + conciseOptBool(ni.PCP, "C")
}

func conciseOptBool(b opt.Bool, trueVal string) string {
	if b == "" {
		return "_"
	}
	v, ok := b.Get()
	if !ok {
		return "x"
	}
	if v {
		return trueVal
	}
	return ""
}

// BasicallyEqual reports whether ni and ni2 are basically equal, ignoring
// changes in DERP ServerLatency & RegionLatency.
func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool {
	if (ni == nil) != (ni2 == nil) {
		return false
	}
	if ni == nil {
		return true
	}
	return ni.MappingVariesByDestIP == ni2.MappingVariesByDestIP &&
		ni.WorkingIPv6 == ni2.WorkingIPv6 &&
		ni.OSHasIPv6 == ni2.OSHasIPv6 &&
		ni.WorkingUDP == ni2.WorkingUDP &&
		ni.WorkingICMPv4 == ni2.WorkingICMPv4 &&
		ni.HavePortMap == ni2.HavePortMap &&
		ni.UPnP == ni2.UPnP &&
		ni.PMP == ni2.PMP &&
		ni.PCP == ni2.PCP &&
		ni.PreferredDERP == ni2.PreferredDERP &&
		ni.LinkType == ni2.LinkType &&
		ni.FirewallMode == ni2.FirewallMode
}

// Equal reports whether h and h2 are equal.
func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
	if h == nil && h2 == nil {
		return true
	}
	if (h == nil) != (h2 == nil) {
		return false
	}
	return reflect.DeepEqual(h, h2)
}

// SignatureType specifies a scheme for signing RegisterRequest messages. It
// specifies the crypto algorithms to use, the contents of what is signed, and
// any other relevant details. Historically, requests were unsigned so the zero
// value is SignatureNone.
type SignatureType int

const (
	// SignatureNone indicates that there is no signature, no Timestamp is
	// required (but may be specified if desired), and both DeviceCert and
	// Signature should be empty.
	SignatureNone = SignatureType(iota)
	// SignatureUnknown represents an unknown signature scheme, which should
	// be considered an error if seen.
	SignatureUnknown
	// SignatureV1 is computed as RSA-PSS-Sign(privateKeyForDeviceCert,
	// SHA256(Timestamp || ServerIdentity || DeviceCert || ServerShortPubKey ||
	// MachineShortPubKey)). The PSS salt length is equal to hash length
	// (rsa.PSSSaltLengthEqualsHash). Device cert is required.
	// Deprecated: uses old key serialization format.
	SignatureV1
	// SignatureV2 is computed as RSA-PSS-Sign(privateKeyForDeviceCert,
	// SHA256(Timestamp || ServerIdentity || DeviceCert || ServerPubKey ||
	// MachinePubKey)). The PSS salt length is equal to hash length
	// (rsa.PSSSaltLengthEqualsHash). Device cert is required.
	SignatureV2
)

func (st SignatureType) AppendText(b []byte) ([]byte, error) {
	return append(b, st.String()...), nil
}

func (st SignatureType) MarshalText() ([]byte, error) {
	return []byte(st.String()), nil
}

func (st *SignatureType) UnmarshalText(b []byte) error {
	switch string(b) {
	case "signature-none":
		*st = SignatureNone
	case "signature-v1":
		*st = SignatureV1
	case "signature-v2":
		*st = SignatureV2
	default:
		var val int
		if _, err := fmt.Sscanf(string(b), "signature-unknown(%d)", &val); err != nil {
			*st = SignatureType(val)
		} else {
			*st = SignatureUnknown
		}
	}
	return nil
}

func (st SignatureType) String() string {
	switch st {
	case SignatureNone:
		return "signature-none"
	case SignatureUnknown:
		return "signature-unknown"
	case SignatureV1:
		return "signature-v1"
	case SignatureV2:
		return "signature-v2"
	default:
		return fmt.Sprintf("signature-unknown(%d)", int(st))
	}
}

// RegisterResponseAuth is the authentication information returned by the server
// in response to a RegisterRequest.
type RegisterResponseAuth struct {
	_ structs.Incomparable

	// At most one of Oauth2Token or AuthKey is set.

	Oauth2Token *Oauth2Token `json:",omitempty"` // used by pre-1.66 Android only
	AuthKey     string       `json:",omitempty"`
}

// RegisterRequest is a request to register a key for a node.
//
// This is JSON-encoded and sent over the control plane connection to:
//
//	POST https://<control-plane>/machine/register.
type RegisterRequest struct {
	_ structs.Incomparable

	// Version is the client's capabilities when using the Noise
	// transport.
	//
	// When using the original nacl crypto_box transport, the
	// value must be 1.
	Version CapabilityVersion

	NodeKey    key.NodePublic
	OldNodeKey key.NodePublic
	NLKey      key.NLPublic
	Auth       *RegisterResponseAuth `json:",omitempty"`
	// Expiry optionally specifies the requested key expiry.
	// The server policy may override.
	// As a special case, if Expiry is in the past and NodeKey is
	// the node's current key, the key is expired.
	Expiry   time.Time
	Followup string // response waits until AuthURL is visited
	Hostinfo *Hostinfo

	// Ephemeral is whether the client is requesting that this
	// node be considered ephemeral and be automatically deleted
	// when it stops being active.
	Ephemeral bool `json:",omitempty"`

	// NodeKeySignature is the node's own node-key signature, re-signed
	// for its new node key using its network-lock key.
	//
	// This field is set when the client retries registration after learning
	// its NodeKeySignature (which is in need of rotation).
	NodeKeySignature tkatype.MarshaledSignature

	// The following fields are not used for SignatureNone and are required for
	// SignatureV1:
	SignatureType SignatureType `json:",omitempty"`
	Timestamp     *time.Time    `json:",omitempty"` // creation time of request to prevent replay
	DeviceCert    []byte        `json:",omitempty"` // X.509 certificate for client device
	Signature     []byte        `json:",omitempty"` // as described by SignatureType

	// Tailnet is an optional identifier specifying the name of the recommended or required
	// network that the node should join. Its exact form should not be depended on; new
	// forms are coming later. The identifier is generally a domain name (for an organization)
	// or e-mail address (for a personal account on a shared e-mail provider). It is the same name
	// used by the API, as described in /api.md#tailnet.
	// If Tailnet begins with the prefix "required:" then the server should prevent logging in to a different
	// network than the one specified. Otherwise, the server should recommend the specified network
	// but still permit logging in to other networks.
	// If empty, no recommendation is offered to the server and the login page should show all options.
	Tailnet string `json:",omitempty"`
}

// RegisterResponse is returned by the server in response to a RegisterRequest.
type RegisterResponse struct {
	User              User
	Login             Login
	NodeKeyExpired    bool   // if true, the NodeKey needs to be replaced
	MachineAuthorized bool   // TODO(crawshaw): move to using MachineStatus
	AuthURL           string // if set, authorization pending

	// If set, this is the current node-key signature that needs to be
	// re-signed for the node's new node-key.
	NodeKeySignature tkatype.MarshaledSignature

	// Error indicates that authorization failed. If this is non-empty,
	// other status fields should be ignored.
	Error string
}

// EndpointType distinguishes different sources of MapRequest.Endpoint values.
type EndpointType int

const (
	EndpointUnknownType    = EndpointType(0)
	EndpointLocal          = EndpointType(1)
	EndpointSTUN           = EndpointType(2)
	EndpointPortmapped     = EndpointType(3)
	EndpointSTUN4LocalPort = EndpointType(4) // hard NAT: STUN'ed IPv4 address + local fixed port
	EndpointExplicitConf   = EndpointType(5) // explicitly configured (routing to be done by client)
)

func (et EndpointType) String() string {
	switch et {
	case EndpointUnknownType:
		return "?"
	case EndpointLocal:
		return "local"
	case EndpointSTUN:
		return "stun"
	case EndpointPortmapped:
		return "portmap"
	case EndpointSTUN4LocalPort:
		return "stun4localport"
	case EndpointExplicitConf:
		return "explicitconf"
	}
	return "other"
}

// Endpoint is an endpoint IPPort and an associated type.
// It doesn't currently go over the wire as is but is instead
// broken up into two parallel slices in MapRequest, for compatibility
// reasons. But this type is used in the codebase.
type Endpoint struct {
	Addr netip.AddrPort
	Type EndpointType
}

// MapRequest is sent by a client to either update the control plane
// about its current state, or to start a long-poll of network map updates.
//
// The request includes a copy of the client's current set of WireGuard
// endpoints and general host information.
//
// This is JSON-encoded and sent over the control plane connection to:
//
//	POST https://<control-plane>/machine/map
type MapRequest struct {
	// Version is incremented whenever the client code changes enough that
	// we want to signal to the control server that we're capable of something
	// different.
	//
	// For current values and history, see the CapabilityVersion type's docs.
	Version CapabilityVersion

	Compress  string `json:",omitzero"` // "zstd" or "" (no compression)
	KeepAlive bool   `json:",omitzero"` // whether server should send keep-alives back to us
	NodeKey   key.NodePublic
	DiscoKey  key.DiscoPublic

	// HardwareAttestationKey is the public key of the node's hardware-backed
	// identity attestation key, if any.
	HardwareAttestationKey key.HardwareAttestationPublic `json:",omitzero"`
	// HardwareAttestationKeySignature is the signature of
	// "$UNIX_TIMESTAMP|$NODE_KEY" using its hardware attestation key, if any.
	HardwareAttestationKeySignature []byte `json:",omitempty"`
	// HardwareAttestationKeySignatureTimestamp is the time at which the
	// HardwareAttestationKeySignature was created, if any. This UNIX timestamp
	// value is prepended to the node key when signing.
	HardwareAttestationKeySignatureTimestamp time.Time `json:",omitzero"`

	// Stream is whether the client wants to receive multiple MapResponses over
	// the same HTTP connection.
	//
	// If false, the server will send a single MapResponse and then close the
	// connection.
	//
	// If true and Version >= 68, the server should treat this as a read-only
	// request and ignore any Hostinfo or other fields that might be set.
	Stream bool `json:",omitzero"`

	// Hostinfo is the client's current Hostinfo. Although it is always included
	// in the request, the server may choose to ignore it when Stream is true
	// and Version >= 68.
	Hostinfo *Hostinfo

	// MapSessionHandle, if non-empty, is a request to reattach to a previous
	// map session after a previous map session was interrupted for whatever
	// reason. Its value is an opaque string as returned by
	// MapResponse.MapSessionHandle.
	//
	// When set, the client must also send MapSessionSeq to specify the last
	// processed message in that prior session.
	//
	// The server may choose to ignore the request for any reason and start a
	// new map session. This is only applicable when Stream is true.
	MapSessionHandle string `json:",omitzero"`

	// MapSessionSeq is the sequence number in the map session identified by
	// MapSesssionHandle that was most recently processed by the client.
	// It is only applicable when MapSessionHandle is specified.
	// If the server chooses to honor the MapSessionHandle request, only sequence
	// numbers greater than this value will be returned.
	MapSessionSeq int64 `json:",omitzero"`

	// Endpoints are the client's magicsock UDP ip:port endpoints (IPv4 or IPv6).
	// These can be ignored if Stream is true and Version >= 68.
	Endpoints []netip.AddrPort `json:",omitempty"`
	// EndpointTypes are the types of the corresponding endpoints in Endpoints.
	EndpointTypes []EndpointType `json:",omitempty"`

	// TKAHead describes the hash of the latest AUM applied to the local
	// tailnet key authority, if one is operating.
	// It is encoded as tka.AUMHash.MarshalText.
	TKAHead string `json:",omitzero"`

	// ReadOnly was set when client just wanted to fetch the MapResponse,
	// without updating their Endpoints. The intended use was for clients to
	// discover the DERP map at start-up before their first real endpoint
	// update.
	//
	// Deprecated: always false as of Version 68.
	ReadOnly bool `json:",omitzero"`

	// OmitPeers is whether the client is okay with the Peers list being omitted
	// in the response.
	//
	// The behavior of OmitPeers being true varies based on Stream and ReadOnly:
	//
	// If OmitPeers is true, Stream is false, and ReadOnly is false,
	// then the server will let clients update their endpoints without
	// breaking existing long-polling (Stream == true) connections.
	// In this case, the server can omit the entire response; the client
	// only checks the HTTP response status code.
	//
	// If OmitPeers is true, Stream is false, but ReadOnly is true,
	// then all the response fields are included. (This is what the client does
	// when initially fetching the DERP map.)
	OmitPeers bool `json:",omitzero"`

	// DebugFlags is a list of strings specifying debugging and
	// development features to enable in handling this map
	// request. The values are deliberately unspecified, as they get
	// added and removed all the time during development, and offer no
	// compatibility promise. To roll out semantic changes, bump
	// Version instead.
	//
	// Current DebugFlags values are:
	//     * "warn-ip-forwarding-off": client is trying to be a subnet
	//       router but their IP forwarding is broken.
	//     * "warn-router-unhealthy": client's Router implementation is
	//       having problems.
	DebugFlags []string `json:",omitempty"`

	// ConnectionHandleForTest, if non-empty, is an opaque string sent by the client that
	// identifies this specific connection to the server. The server may choose to
	// use this handle to identify the connection for debugging or testing
	// purposes. It has no semantic meaning.
	ConnectionHandleForTest string `json:",omitzero"`
}

// PortRange represents a range of UDP or TCP port numbers.
type PortRange struct {
	First uint16
	Last  uint16
}

// Contains reports whether port is in pr.
func (pr PortRange) Contains(port uint16) bool {
	return port >= pr.First && port <= pr.Last
}

var PortRangeAny = PortRange{0, 65535}

func (pr PortRange) String() string {
	if pr.First == pr.Last {
		return strconv.FormatUint(uint64(pr.First), 10)
	} else if pr == PortRangeAny {
		return "*"
	}
	return fmt.Sprintf("%d-%d", pr.First, pr.Last)
}

// NetPortRange represents a range of ports that's allowed for one or more IPs.
type NetPortRange struct {
	_     structs.Incomparable
	IP    string // IP, CIDR, Range, or "*" (same formats as FilterRule.SrcIPs)
	Bits  *int   `json:",omitempty"` // deprecated; the 2020 way to turn IP into a CIDR. See FilterRule.SrcBits.
	Ports PortRange
}

// CapGrant grants capabilities in a FilterRule.
type CapGrant struct {
	// Dsts are the destination IP ranges that this capability
	// grant matches.
	Dsts []netip.Prefix

	// Caps are the capabilities the source IP matched by
	// FilterRule.SrcIPs are granted to the destination IP,
	// matched by Dsts.
	// Deprecated: use CapMap instead.
	Caps []PeerCapability `json:",omitempty"`

	// CapMap is a map of capabilities to their values.
	// The key is the capability name, and the value is a list of
	// values for that capability.
	CapMap PeerCapMap `json:",omitempty"`
}

// PeerCapability represents a capability granted to a peer by a FilterRule when
// the peer communicates with the node that has this rule. Its meaning is
// application-defined.
//
// It must be a URL like "https://tailscale.com/cap/file-send".
type PeerCapability string

const (
	// PeerCapabilityFileSharingTarget grants the current node the ability to send
	// files to the peer which has this capability.
	PeerCapabilityFileSharingTarget PeerCapability = "https://tailscale.com/cap/file-sharing-target"
	// PeerCapabilityFileSharingSend grants the ability to receive files from a
	// node that's owned by a different user.
	PeerCapabilityFileSharingSend PeerCapability = "https://tailscale.com/cap/file-send"
	// PeerCapabilityDebugPeer grants the ability for a peer to read this node's
	// goroutines, metrics, magicsock internal state, etc.
	PeerCapabilityDebugPeer PeerCapability = "https://tailscale.com/cap/debug-peer"
	// PeerCapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet.
	PeerCapabilityWakeOnLAN PeerCapability = "https://tailscale.com/cap/wake-on-lan"
	// PeerCapabilityIngress grants the ability for a peer to send ingress traffic.
	PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress"
	// PeerCapabilityWebUI grants the ability for a peer to edit features from the
	// device Web UI.
	PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui"
	// PeerCapabilityTaildrive grants the ability for a peer to access Taildrive
	// shares.
	PeerCapabilityTaildrive PeerCapability = "tailscale.com/cap/drive"
	// PeerCapabilityTaildriveSharer indicates that a peer has the ability to
	// share folders with us.
	PeerCapabilityTaildriveSharer PeerCapability = "tailscale.com/cap/drive-sharer"

	// PeerCapabilityKubernetes grants a peer Kubernetes-specific
	// capabilities, such as the ability to impersonate specific Tailscale
	// user groups as Kubernetes user groups. This capability is read by
	// peers that are Tailscale Kubernetes operator instances.
	PeerCapabilityKubernetes PeerCapability = "tailscale.com/cap/kubernetes"

	// PeerCapabilityRelay grants the ability for a peer to allocate relay
	// endpoints.
	PeerCapabilityRelay PeerCapability = "tailscale.com/cap/relay"
	// PeerCapabilityRelayTarget grants the current node the ability to allocate
	// relay endpoints to the peer which has this capability.
	PeerCapabilityRelayTarget PeerCapability = "tailscale.com/cap/relay-target"

	// PeerCapabilityTsIDP grants a peer tsidp-specific
	// capabilities, such as the ability to add user groups to the OIDC
	// claim
	PeerCapabilityTsIDP PeerCapability = "tailscale.com/cap/tsidp"
)

// NodeCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the [NodeCapMap.Contains] method.
//
// See [NodeCapability] for more information on keys.
type NodeCapMap map[NodeCapability][]RawMessage

// Equal reports whether c and c2 are equal.
func (c NodeCapMap) Equal(c2 NodeCapMap) bool {
	return maps.EqualFunc(c, c2, slices.Equal)
}

// UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap NodeCapability) ([]T, error) {
	return UnmarshalNodeCapViewJSON[T](views.MapSliceOf(cm), cap)
}

// UnmarshalNodeCapViewJSON unmarshals each JSON value in cm.Get(cap) as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[NodeCapability, RawMessage], cap NodeCapability) ([]T, error) {
	vals, ok := cm.GetOk(cap)
	if !ok {
		return nil, nil
	}
	out := make([]T, 0, vals.Len())
	for _, v := range vals.All() {
		var t T
		if err := json.Unmarshal([]byte(v), &t); err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, nil
}

// Contains reports whether c has the capability cap. This is used to test for
// the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c NodeCapMap) Contains(cap NodeCapability) bool {
	_, ok := c[cap]
	return ok
}

// PeerCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the HasCapability method.
//
// The values are opaque to Tailscale, but are passed through from the ACLs to
// the application via the WhoIs API.
type PeerCapMap map[PeerCapability][]RawMessage

// UnmarshalCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) {
	return UnmarshalCapViewJSON[T](views.MapSliceOf(cm), cap)
}

// UnmarshalCapViewJSON unmarshals each JSON value in cm.Get(cap) as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalCapViewJSON[T any](cm views.MapSlice[PeerCapability, RawMessage], cap PeerCapability) ([]T, error) {
	vals, ok := cm.GetOk(cap)
	if !ok {
		return nil, nil
	}
	out := make([]T, 0, vals.Len())
	for _, v := range vals.All() {
		var t T
		if err := json.Unmarshal([]byte(v), &t); err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, nil
}

// HasCapability reports whether c has the capability cap. This is used to test
// for the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c PeerCapMap) HasCapability(cap PeerCapability) bool {
	_, ok := c[cap]
	return ok
}

// FilterRule represents one rule in a packet filter.
//
// A rule is logically a set of source CIDRs to match (described by
// SrcIPs), and a set of destination targets that are then
// allowed if a source IP is matches of those CIDRs.
type FilterRule struct {
	// SrcIPs are the source IPs/networks to match.
	//
	// It may take the following forms:
	//     * an IP address (IPv4 or IPv6)
	//     * the string "*" to match everything (both IPv4 & IPv6)
	//     * a CIDR (e.g. "192.168.0.0/16")
	//     * a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800")
	//     * a string "cap:<capability>" with NodeCapMap cap name
	SrcIPs []string

	// SrcBits is deprecated; it was the old way to specify a CIDR
	// prior to CapabilityVersion 7. Its values correspond to the
	// SrcIPs above.
	//
	// If an entry of SrcBits is present for the same index as a
	// SrcIPs entry, it changes the SrcIP above to be a network
	// with /n CIDR bits. If the slice is nil or insufficiently
	// long, the default value (for an IPv4 address) for a
	// position is 32, as if the SrcIPs above were a /32 mask. For
	// a "*" SrcIPs value, the corresponding SrcBits value is
	// ignored.
	//
	// This is still present in this file because the Tailscale control plane
	// code still uses this type, for 118 clients that are still connected as of
	// 2024-06-18, 3.5 years after the last release that used this type.
	SrcBits []int `json:",omitempty"`

	// DstPorts are the port ranges to allow once a source IP
	// matches (is in the CIDR described by SrcIPs).
	//
	// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
	DstPorts []NetPortRange `json:",omitempty"`

	// IPProto are the IP protocol numbers to match.
	//
	// As a special case, nil or empty means TCP, UDP, and ICMP.
	//
	// Numbers outside the uint8 range (below 0 or above 255) are
	// reserved for Tailscale's use. Unknown ones are ignored.
	//
	// Depending on the IPProto values, DstPorts may or may not be
	// used.
	IPProto []int `json:",omitempty"`

	// CapGrant, if non-empty, are the capabilities to
	// conditionally grant to the source IP in SrcIPs.
	//
	// Think of DstPorts as "capabilities for networking" and
	// CapGrant as arbitrary application-defined capabilities
	// defined between the admin's ACLs and the application
	// doing WhoIs lookups, looking up the remote IP address's
	// application-level capabilities.
	//
	// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
	CapGrant []CapGrant `json:",omitempty"`
}

var FilterAllowAll = []FilterRule{
	{
		SrcIPs: []string{"*"},
		DstPorts: []NetPortRange{{
			IP:    "*",
			Ports: PortRange{0, 65535},
		}},
	},
}

// DNSConfig is the DNS configuration.
type DNSConfig struct {
	// Resolvers are the DNS resolvers to use, in order of preference.
	Resolvers []*dnstype.Resolver `json:",omitempty"`

	// Routes maps DNS name suffixes to a set of DNS resolvers to
	// use. It is used to implement "split DNS" and other advanced DNS
	// routing overlays.
	//
	// Map keys are fully-qualified DNS name suffixes; they may
	// optionally contain a trailing dot but no leading dot.
	//
	// If the value is an empty slice, that means the suffix should still
	// be handled by Tailscale's built-in resolver (100.100.100.100), such
	// as for the purpose of handling ExtraRecords.
	Routes map[string][]*dnstype.Resolver `json:",omitempty"`

	// FallbackResolvers is like Resolvers, but is only used if a
	// split DNS configuration is requested in a configuration that
	// doesn't work yet without explicit default resolvers.
	// https://github.com/tailscale/tailscale/issues/1743
	FallbackResolvers []*dnstype.Resolver `json:",omitempty"`
	// Domains are the search domains to use.
	// Search domains must be FQDNs, but *without* the trailing dot.
	Domains []string `json:",omitempty"`
	// Proxied turns on automatic resolution of hostnames for devices
	// in the network map, aka MagicDNS.
	// Despite the (legacy) name, does not necessarily cause request
	// proxying to be enabled.
	Proxied bool `json:",omitzero"`

	// Nameservers are the IP addresses of the global nameservers to use.
	//
	// Deprecated: this is only set and used by MapRequest.Version >=9 and <14. Use Resolvers instead.
	Nameservers []netip.Addr `json:",omitempty"`

	// CertDomains are the set of DNS names for which the control
	// plane server will assist with provisioning TLS
	// certificates. See SetDNSRequest, which can be used to
	// answer dns-01 ACME challenges for e.g. LetsEncrypt.
	// These names are FQDNs without trailing periods, and without
	// any "_acme-challenge." prefix.
	CertDomains []string `json:",omitempty"`

	// ExtraRecords contains extra DNS records to add to the
	// MagicDNS config.
	ExtraRecords []DNSRecord `json:",omitempty"`

	// ExitNodeFilteredSuffixes are the DNS suffixes that the
	// node, when being an exit node DNS proxy, should not answer.
	//
	// The entries do not contain trailing periods and are always
	// all lowercase.
	//
	// If an entry starts with a period, it's a suffix match (but
	// suffix ".a.b" doesn't match "a.b"; a prefix is required).
	//
	// If an entry does not start with a period, it's an exact
	// match.
	//
	// Matches are case insensitive.
	ExitNodeFilteredSet []string `json:",omitempty"`

	// TempCorpIssue13969 is a temporary (2023-08-16) field for an internal hack day prototype.
	// It contains a user inputed URL that should have a list of domains to be blocked.
	// See https://github.com/tailscale/corp/issues/13969.
	TempCorpIssue13969 string `json:",omitzero"`
}

// DNSRecord is an extra DNS record to add to MagicDNS.
type DNSRecord struct {
	// Name is the fully qualified domain name of
	// the record to add. The trailing dot is optional.
	Name string

	// Type is the DNS record type.
	// Empty means A or AAAA, depending on value.
	// Other values are currently ignored.
	Type string `json:",omitzero"`

	// Value is the IP address in string form.
	// TODO(bradfitz): if we ever add support for record types
	// with non-UTF8 binary data, add ValueBytes []byte that
	// would take precedence.
	Value string
}

// PingType is a string representing the kind of ping to perform.
type PingType string

const (
	// PingDisco performs a ping, without involving IP at either end.
	PingDisco PingType = "disco"
	// PingTSMP performs a ping, using the IP layer, but avoiding the OS IP stack.
	PingTSMP PingType = "TSMP"
	// PingICMP performs a ping between two tailscale nodes using ICMP that is
	// received by the target systems IP stack.
	PingICMP PingType = "ICMP"
	// PingPeerAPI performs a ping between two tailscale nodes using ICMP that is
	// received by the target systems IP stack.
	PingPeerAPI PingType = "peerapi"
)

// PingRequest is a request from the control plane to the local node to probe
// something.
//
// A PingRequest with no IP and Types is a request from the control plane to the
// local node to send an HTTP request to a URL to prove the long-polling client
// is still connected.
//
// A PingRequest with Types and IP, will send a ping to the IP and send a POST
// request containing a PingResponse to the URL containing results.
type PingRequest struct {
	// URL is the URL to reply to the PingRequest to.
	// It will be a unique URL each time. No auth headers are necessary.
	// If the client sees multiple PingRequests with the same URL,
	// subsequent ones should be ignored.
	//
	// The HTTP method that the node should make back to URL depends on the other
	// fields of the PingRequest. If Types is defined, then URL is the URL to
	// send a POST request to. Otherwise, the node should just make a HEAD
	// request to URL.
	URL string

	// URLIsNoise, if true, means that the client should hit URL over the Noise
	// transport instead of TLS.
	URLIsNoise bool `json:",omitzero"`

	// Log is whether to log about this ping in the success case.
	// For failure cases, the client will log regardless.
	Log bool `json:",omitzero"`

	// Types is the types of ping that are initiated. Can be any PingType, comma
	// separated, e.g. "disco,TSMP"
	//
	// As a special case, if Types is "c2n", then this PingRequest is a
	// client-to-node HTTP request. The HTTP request should be handled by this
	// node's c2n handler and the HTTP response sent in a POST to URL. For c2n,
	// the value of URLIsNoise is ignored and only the Noise transport (back to
	// the control plane) will be used, as if URLIsNoise were true.
	Types string `json:",omitzero"`

	// IP is the ping target, when needed by the PingType(s) given in Types.
	IP netip.Addr `json:",omitzero"`

	// Payload is the ping payload.
	//
	// It is only used for c2n requests, in which case it's an HTTP/1.0 or
	// HTTP/1.1-formatted HTTP request as parsable with http.ReadRequest.
	Payload []byte `json:",omitempty"`
}

// PingResponse provides result information for a TSMP or Disco PingRequest.
// Typically populated from an ipnstate.PingResult used in `tailscale ping`.
type PingResponse struct {
	Type PingType // ping type, such as TSMP or disco.

	IP       string `json:",omitempty"` // ping destination
	NodeIP   string `json:",omitempty"` // Tailscale IP of node handling IP (different for subnet routers)
	NodeName string `json:",omitempty"` // DNS name base or (possibly not unique) hostname

	// Err contains a short description of error conditions if the PingRequest
	// could not be fulfilled for some reason.
	// e.g. "100.1.2.3 is local Tailscale IP"
	Err string `json:",omitempty"`

	// LatencySeconds reports measurement of the round-trip time of a message to
	// the requested target, if it could be determined. If LatencySeconds is
	// omitted, Err should contain information as to the cause.
	LatencySeconds float64 `json:",omitempty"`

	// Endpoint is a string of the form "{ip}:{port}" if direct UDP was used. It
	// is not currently set for TSMP.
	Endpoint string `json:",omitempty"`

	// PeerRelay is a string of the form "{ip}:{port}:vni:{vni}" if a peer
	// relay was used. It is not currently set for TSMP.
	PeerRelay string `json:",omitempty"`

	// DERPRegionID is non-zero DERP region ID if DERP was used.
	// It is not currently set for TSMP pings.
	DERPRegionID int `json:",omitempty"`

	// DERPRegionCode is the three-letter region code
	// corresponding to DERPRegionID.
	// It is not currently set for TSMP pings.
	DERPRegionCode string `json:",omitempty"`

	// PeerAPIPort is set by TSMP ping responses for peers that
	// are running a peerapi server. This is the port they're
	// running the server on.
	PeerAPIPort uint16 `json:",omitempty"`

	// IsLocalIP is whether the ping request error is due to it being
	// a ping to the local node.
	IsLocalIP bool `json:",omitempty"`
}

// MapResponse is the response to a MapRequest. It describes the state of the
// local node, the peer nodes, the DNS configuration, the packet filter, and
// more. A MapRequest, depending on its parameters, may result in the control
// plane coordination server sending 0, 1 or a stream of multiple MapResponse
// values.
//
// When the client sets MapRequest.Stream, the server sends a stream of
// MapResponses. That long-lived HTTP transaction is called a "map poll". In a
// map poll, the first MapResponse will be complete and subsequent MapResponses
// will be incremental updates with only changed information.
//
// The zero value for all fields means "unchanged". Unfortunately, several
// fields were defined before that convention was established, so they use a
// slice with omitempty, meaning this type can't be used to marshal JSON
// containing non-nil zero-length slices (meaning explicitly now empty). The
// control plane uses a separate type to marshal these fields. This type is
// primarily used for unmarshaling responses so the omitempty annotations are
// mostly useless, except that this type is also used for the integration test's
// fake control server. (It's not necessary to marshal a non-nil zero-length
// slice for the things we've needed to test in the integration tests as of
// 2023-09-09).
type MapResponse struct {
	// MapSessionHandle optionally specifies a unique opaque handle for this
	// stateful MapResponse session. Servers may choose not to send it, and it's
	// only sent on the first MapResponse in a stream. The client can determine
	// whether it's reattaching to a prior stream by seeing whether this value
	// matches the requested MapRequest.MapSessionHandle.
	MapSessionHandle string `json:",omitempty"`

	// Seq is a sequence number within a named map session (a response where the
	// first message contains a MapSessionHandle). The Seq number may be omitted
	// on responses that don't change the state of the stream, such as KeepAlive
	// or certain types of PingRequests. This is the value to be sent in
	// MapRequest.MapSessionSeq to resume after this message.
	Seq int64 `json:",omitempty"`

	// KeepAlive, if set, represents an empty message just to keep
	// the connection alive. When true, all other fields except
	// PingRequest, ControlTime, and PopBrowserURL are ignored.
	KeepAlive bool `json:",omitempty"`

	// PingRequest, if non-empty, is a request to the client to
	// prove it's still there by sending an HTTP request to the
	// provided URL. No auth headers are necessary.
	// PingRequest may be sent on any MapResponse (ones with
	// KeepAlive true or false).
	PingRequest *PingRequest `json:",omitempty"`

	// PopBrowserURL, if non-empty, is a URL for the client to
	// open to complete an action. The client should dup suppress
	// identical URLs and only open it once for the same URL.
	PopBrowserURL string `json:",omitempty"`

	// Networking

	// Node describes the node making the map request.
	// Starting with MapRequest.Version 18, nil means unchanged.
	Node *Node `json:",omitempty"`

	// DERPMap describe the set of DERP servers available.
	// A nil value means unchanged.
	DERPMap *DERPMap `json:",omitempty"`

	// Peers, if non-empty, is the complete list of peers.
	// It will be set in the first MapResponse for a long-polled request/response.
	// Subsequent responses will be delta-encoded if MapRequest.Version >= 5 and server
	// chooses, in which case Peers will be nil or zero length.
	// If Peers is non-empty, PeersChanged and PeersRemoved should
	// be ignored (and should be empty).
	// Peers is always returned sorted by Node.ID.
	Peers []*Node `json:",omitempty"`
	// PeersChanged are the Nodes (identified by their ID) that
	// have changed or been added since the past update on the
	// HTTP response. It's not used by the server if MapRequest.Version < 5.
	// PeersChanged is always returned sorted by Node.ID.
	PeersChanged []*Node `json:",omitempty"`
	// PeersRemoved are the NodeIDs that are no longer in the peer list.
	PeersRemoved []NodeID `json:",omitempty"`

	// PeersChangedPatch, if non-nil, means that node(s) have changed.
	// This is a lighter version of the older PeersChanged support that
	// only supports certain types of updates.
	//
	// These are applied after Peers* above, but in practice the
	// control server should only send these on their own, without
	// the Peers* fields also set.
	PeersChangedPatch []*PeerChange `json:",omitempty"`

	// PeerSeenChange contains information on how to update peers' LastSeen
	// times. If the value is false, the peer is gone. If the value is true,
	// the LastSeen time is now. Absent means unchanged.
	PeerSeenChange map[NodeID]bool `json:",omitempty"`

	// OnlineChange changes the value of a Peer Node.Online value.
	OnlineChange map[NodeID]bool `json:",omitempty"`

	// DNSConfig contains the DNS settings for the client to use.
	// A nil value means no change from an earlier non-nil value.
	DNSConfig *DNSConfig `json:",omitempty"`

	// Domain is the name of the network that this node is
	// in. It's either of the form "example.com" (for user
	// foo@example.com, for multi-user networks) or
	// "foo@gmail.com" (for siloed users on shared email
	// providers). Its exact form should not be depended on; new
	// forms are coming later.
	// If empty, the value is unchanged.
	Domain string `json:",omitempty"`

	// CollectServices reports whether this node's Tailnet has
	// requested that info about services be included in HostInfo.
	// If unset, the most recent non-empty MapResponse value in
	// the HTTP response stream is used.
	CollectServices opt.Bool `json:",omitempty"`

	// PacketFilter are the firewall rules.
	//
	// For MapRequest.Version >= 6, a nil value means the most
	// previously streamed non-nil MapResponse.PacketFilter within
	// the same HTTP response. A non-nil but empty list always means
	// no PacketFilter (that is, to block everything).
	//
	// Note that this package's type, due its use of a slice and omitempty, is
	// unable to marshal a zero-length non-nil slice. The control server needs
	// to marshal this type using a separate type. See MapResponse docs.
	//
	// See PacketFilters for the newer way to send PacketFilter updates.
	PacketFilter []FilterRule `json:",omitempty"`

	// PacketFilters encodes incremental packet filter updates to the client
	// without having to send the entire packet filter on any changes as
	// required by the older PacketFilter (singular) field above. The map keys
	// are server-assigned arbitrary strings. The map values are the new rules
	// for that key, or nil to delete it. The client then concatenates all the
	// rules together to generate the final packet filter. Because the
	// FilterRules can only match or not match, the ordering of filter rules
	// doesn't matter. (That said, the client generates the file merged packet
	// filter rules by concananting all the packet filter rules sorted by the
	// map key name. But it does so for stability and testability, not
	// correctness. If something needs to rely on that property, something has
	// gone wrong.)
	//
	// If the server sends a non-nil PacketFilter (above), that is equivalent to
	// a named packet filter with the key "base". It is valid for the server to
	// send both PacketFilter and PacketFilters in the same MapResponse or
	// alternate between them within a session. The PacketFilter is applied
	// first (if set) and then the PacketFilters.
	//
	// As a special case, the map key "*" with a value of nil means to clear all
	// prior named packet filters (including any implicit "base") before
	// processing the other map entries.
	PacketFilters map[string][]FilterRule `json:",omitempty"`

	// UserProfiles are the user profiles of nodes in the network.
	// As as of 1.1.541 (mapver 5), this contains new or updated
	// user profiles only.
	UserProfiles []UserProfile `json:",omitempty"`

	// Health, if non-nil, sets the health state of the node from the control
	// plane's perspective. A nil value means no change from the previous
	// MapResponse. A non-nil 0-length slice restores the health to good (no
	// known problems). A non-zero length slice are the list of problems that
	// the control plane sees.
	//
	// Either this will be set, or DisplayMessages will be set, but not both.
	//
	// Note that this package's type, due its use of a slice and omitempty, is
	// unable to marshal a zero-length non-nil slice. The control server needs
	// to marshal this type using a separate type. See MapResponse docs.
	Health []string `json:",omitempty"`

	// DisplayMessages sets the health state of the node from the control
	// plane's perspective.
	//
	// Either this will be set, or Health will be set, but not both.
	//
	// The map keys are IDs that uniquely identify the type of health issue. The
	// map values are the messages. If the server sends down a map with entries,
	// the client treats it as a patch: new entries are added, keys with a value
	// of nil are deleted, existing entries with new values are updated. A nil
	// map and an empty map both mean no change has occurred since the last
	// update.
	//
	// As a special case, the map key "*" with a value of nil means to clear all
	// prior display messages before processing the other map entries.
	DisplayMessages map[DisplayMessageID]*DisplayMessage `json:",omitempty"`

	// SSHPolicy, if non-nil, updates the SSH policy for how incoming
	// SSH connections should be handled.
	SSHPolicy *SSHPolicy `json:",omitempty"`

	// ControlTime, if non-zero, is the current timestamp according to the control server.
	ControlTime *time.Time `json:",omitempty"`

	// TKAInfo describes the control plane's view of tailnet
	// key authority (TKA) state.
	//
	// An initial nil TKAInfo indicates that the control plane
	// believes TKA should not be enabled. An initial non-nil TKAInfo
	// indicates the control plane believes TKA should be enabled.
	// A nil TKAInfo in a mapresponse stream (i.e. a 'delta' mapresponse)
	// indicates no change from the value sent earlier.
	TKAInfo *TKAInfo `json:",omitempty"`

	// DomainDataPlaneAuditLogID, if non-empty, is the per-tailnet log ID to be
	// used when writing data plane audit logs.
	DomainDataPlaneAuditLogID string `json:",omitempty"`

	// Debug is normally nil, except for when the control server
	// is setting debug settings on a node.
	Debug *Debug `json:",omitempty"`

	// ControlDialPlan tells the client how to connect to the control
	// server. An initial nil is equivalent to new(ControlDialPlan).
	// A subsequent streamed nil means no change.
	ControlDialPlan *ControlDialPlan `json:",omitempty"`

	// ClientVersion describes the latest client version that's available for
	// download and whether the client is using it. A nil value means no change
	// or nothing to report.
	ClientVersion *ClientVersion `json:",omitempty"`

	// DeprecatedDefaultAutoUpdate is the default node auto-update setting for this
	// tailnet. The node is free to opt-in or out locally regardless of this
	// value. Once this value has been set and stored in the client, future
	// changes from the control plane are ignored.
	//
	// Deprecated: use NodeAttrDefaultAutoUpdate instead. See
	// https://github.com/tailscale/tailscale/issues/11502.
	DeprecatedDefaultAutoUpdate opt.Bool `json:"DefaultAutoUpdate,omitempty"`
}

// DisplayMessage represents a health state of the node from the control plane's
// perspective. It is deliberately similar to [health.Warnable] as both get
// converted into [health.UnhealthyState] to be sent to the GUI.
type DisplayMessage struct {
	// Title is a string that the GUI uses as title for this message. The title
	// should be short and fit in a single line. It should not end in a period.
	//
	// Example: "Network may be blocking Tailscale".
	//
	// See the various instantiations of [health.Warnable] for more examples.
	Title string

	// Text is an extended string that the GUI will display to the user. This
	// could be multiple sentences explaining the issue in more detail.
	//
	// Example: "macOS Screen Time seems to be blocking Tailscale. Try disabling
	// Screen Time in System Settings > Screen Time > Content & Privacy > Access
	// to Web Content."
	//
	// See the various instantiations of [health.Warnable] for more examples.
	Text string

	// Severity is the severity of the DisplayMessage, which the GUI can use to
	// determine how to display it. Maps to [health.Severity].
	Severity DisplayMessageSeverity

	// ImpactsConnectivity is whether the health problem will impact the user's
	// ability to connect to the Internet or other nodes on the tailnet, which
	// the GUI can use to determine how to display it.
	ImpactsConnectivity bool `json:",omitempty"`

	// Primary action, if present, represents the action to allow the user to
	// take when interacting with this message. For example, if the
	// DisplayMessage is shown via a notification, the action label might be a
	// button on that notification and clicking the button would open the URL.
	PrimaryAction *DisplayMessageAction `json:",omitempty"`
}

// DisplayMessageAction represents an action (URL and link) to be presented to
// the user associated with a [DisplayMessage].
type DisplayMessageAction struct {
	// URL is the URL to navigate to when the user interacts with this action
	URL string

	// Label is the call to action for the UI to display on the UI element that
	// will open the URL (such as a button or link). For example, "Sign in" or
	// "Learn more".
	Label string
}

// DisplayMessageID is a string that uniquely identifies the kind of health
// issue (e.g. "session-expired").
type DisplayMessageID string

// Equal returns true iff all fields are equal.
func (m DisplayMessage) Equal(o DisplayMessage) bool {
	return m.Title == o.Title &&
		m.Text == o.Text &&
		m.Severity == o.Severity &&
		m.ImpactsConnectivity == o.ImpactsConnectivity &&
		(m.PrimaryAction == nil) == (o.PrimaryAction == nil) &&
		(m.PrimaryAction == nil || (m.PrimaryAction.URL == o.PrimaryAction.URL &&
			m.PrimaryAction.Label == o.PrimaryAction.Label))
}

// DisplayMessageSeverity represents how serious a [DisplayMessage] is. Analogous
// to health.Severity.
type DisplayMessageSeverity string

const (
	// SeverityHigh is the highest severity level, used for critical errors that need immediate attention.
	// On platforms where the client GUI can deliver notifications, a SeverityHigh message will trigger
	// a modal notification.
	SeverityHigh DisplayMessageSeverity = "high"
	// SeverityMedium is used for errors that are important but not critical. This won't trigger a modal
	// notification, however it will be displayed in a more visible way than a SeverityLow message.
	SeverityMedium DisplayMessageSeverity = "medium"
	// SeverityLow is used for less important notices that don't need immediate attention. The user will
	// have to go to a Settings window, or another "hidden" GUI location to see these messages.
	SeverityLow DisplayMessageSeverity = "low"
)

// ClientVersion is information about the latest client version that's available
// for the client (and whether they're already running it).
//
// It does not include a URL to download the client, as that varies by platform.
type ClientVersion struct {
	// RunningLatest is true if the client is running the latest build.
	RunningLatest bool `json:",omitempty"`

	// LatestVersion is the latest version.Short ("1.34.2") version available
	// for download for the client's platform and packaging type.
	// It won't be populated if RunningLatest is true.
	LatestVersion string `json:",omitempty"`

	// UrgentSecurityUpdate is set when the client is missing an important
	// security update. That update may be in LatestVersion or earlier.
	// UrgentSecurityUpdate should not be set if RunningLatest is true.
	UrgentSecurityUpdate bool `json:",omitempty"`

	// Notify is whether the client should do an OS-specific notification about
	// a new version being available. This should not be populated if
	// RunningLatest is true. The client should not notify multiple times for
	// the same LatestVersion value.
	Notify bool `json:",omitempty"`

	// NotifyURL is a URL to open in the browser when the user clicks on the
	// notification, when Notify is true.
	NotifyURL string `json:",omitempty"`

	// NotifyText is the text to show in the notification, when Notify is true.
	NotifyText string `json:",omitempty"`
}

// ControlDialPlan is instructions from the control server to the client on how
// to connect to the control server; this is useful for maintaining connection
// if the client's network state changes after the initial connection, or due
// to the configuration that the control server pushes.
type ControlDialPlan struct {
	// An empty list means the default: use DNS (unspecified which DNS).
	Candidates []ControlIPCandidate
}

// ControlIPCandidate represents a single candidate address to use when
// connecting to the control server.
type ControlIPCandidate struct {
	// IP is the address to attempt connecting to.
	IP netip.Addr `json:",omitzero"`

	// ACEHost, if non-empty, means that the client should connect to the
	// control plane using an HTTPS CONNECT request to the provided hostname. If
	// the IP field is also set, then the IP is the IP address of the ACEHost
	// (and not the control plane) and DNS should not be used. The target (the
	// argument to CONNECT) is always the control plane's hostname, not an IP.
	ACEHost string `json:",omitempty"`

	// DialStartSec is the number of seconds after the beginning of the
	// connection process to wait before trying this candidate.
	DialStartDelaySec float64 `json:",omitempty"`

	// DialTimeoutSec is the timeout for a connection to this candidate,
	// starting after DialStartDelaySec.
	DialTimeoutSec float64 `json:",omitempty"`

	// Priority is the relative priority of this candidate; candidates with
	// a higher priority are preferred over candidates with a lower
	// priority.
	Priority int `json:",omitempty"`
}

// Debug used to be a miscellaneous set of declarative debug config changes and
// imperative debug commands. They've since been mostly migrated to node
// attributes (MapResponse.Node.Capabilities) for the declarative things and c2n
// requests for the imperative things. Not much remains here. Don't add more.
type Debug struct {
	// SleepSeconds requests that the client sleep for the
	// provided number of seconds.
	// The client can (and should) limit the value (such as 5
	// minutes). This exists as a safety measure to slow down
	// spinning clients, in case we introduce a bug in the
	// state machine.
	SleepSeconds float64 `json:",omitempty"`

	// DisableLogTail disables the logtail package. Once disabled it can't be
	// re-enabled for the lifetime of the process.
	//
	// This is primarily used by Headscale.
	DisableLogTail bool `json:",omitempty"`

	// Exit optionally specifies that the client should os.Exit
	// with this code. This is a safety measure in case a client is crash
	// looping or in an unsafe state and we need to remotely shut it down.
	Exit *int `json:",omitempty"`
}

func (id ID) String() string      { return fmt.Sprintf("id:%d", int64(id)) }
func (id UserID) String() string  { return fmt.Sprintf("userid:%d", int64(id)) }
func (id LoginID) String() string { return fmt.Sprintf("loginid:%d", int64(id)) }
func (id NodeID) String() string  { return fmt.Sprintf("nodeid:%d", int64(id)) }

// Equal reports whether n and n2 are equal.
func (n *Node) Equal(n2 *Node) bool {
	if n == nil && n2 == nil {
		return true
	}
	return n != nil && n2 != nil &&
		n.ID == n2.ID &&
		n.StableID == n2.StableID &&
		n.Name == n2.Name &&
		n.User == n2.User &&
		n.Sharer == n2.Sharer &&
		n.UnsignedPeerAPIOnly == n2.UnsignedPeerAPIOnly &&
		n.Key == n2.Key &&
		n.KeyExpiry.Equal(n2.KeyExpiry) &&
		bytes.Equal(n.KeySignature, n2.KeySignature) &&
		n.Machine == n2.Machine &&
		n.DiscoKey == n2.DiscoKey &&
		eqPtr(n.Online, n2.Online) &&
		slicesx.EqualSameNil(n.Addresses, n2.Addresses) &&
		slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) &&
		slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) &&
		slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) &&
		n.LegacyDERPString == n2.LegacyDERPString &&
		n.HomeDERP == n2.HomeDERP &&
		n.Cap == n2.Cap &&
		n.Hostinfo.Equal(n2.Hostinfo) &&
		n.Created.Equal(n2.Created) &&
		eqTimePtr(n.LastSeen, n2.LastSeen) &&
		n.MachineAuthorized == n2.MachineAuthorized &&
		slices.Equal(n.Capabilities, n2.Capabilities) &&
		n.CapMap.Equal(n2.CapMap) &&
		n.ComputedName == n2.ComputedName &&
		n.computedHostIfDifferent == n2.computedHostIfDifferent &&
		n.ComputedNameWithHost == n2.ComputedNameWithHost &&
		slicesx.EqualSameNil(n.Tags, n2.Tags) &&
		n.Expired == n2.Expired &&
		eqPtr(n.SelfNodeV4MasqAddrForThisPeer, n2.SelfNodeV4MasqAddrForThisPeer) &&
		eqPtr(n.SelfNodeV6MasqAddrForThisPeer, n2.SelfNodeV6MasqAddrForThisPeer) &&
		n.IsWireGuardOnly == n2.IsWireGuardOnly &&
		n.IsJailed == n2.IsJailed
}

func eqPtr[T comparable](a, b *T) bool {
	if a == b { // covers nil
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func eqTimePtr(a, b *time.Time) bool {
	return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
}

// Oauth2Token is a copy of golang.org/x/oauth2.Token, to avoid the
// go.mod dependency on App Engine and grpc, which was causing problems.
// All we actually needed was this struct on the client side.
type Oauth2Token struct {
	// AccessToken is the token that authorizes and authenticates
	// the requests.
	AccessToken string `json:"access_token"`

	// TokenType is the type of token.
	// The Type method returns either this or "Bearer", the default.
	TokenType string `json:"token_type,omitempty"`

	// RefreshToken is a token that's used by the application
	// (as opposed to the user) to refresh the access token
	// if it expires.
	RefreshToken string `json:"refresh_token,omitempty"`

	// Expiry is the optional expiration time of the access token.
	//
	// If zero, TokenSource implementations will reuse the same
	// token forever and RefreshToken or equivalent
	// mechanisms for that TokenSource will not be used.
	Expiry time.Time `json:"expiry,omitempty"`
}

// NodeCapability represents a capability granted to the self node as listed in
// MapResponse.Node.Capabilities.
//
// It must be a URL like "https://tailscale.com/cap/file-sharing", or a
// well-known capability name like "funnel". The latter is only allowed for
// Tailscale-defined capabilities.
//
// Unlike PeerCapability, NodeCapability is not in context of a peer and is
// granted to the node itself.
//
// These are also referred to as "Node Attributes" in the ACL policy file.
type NodeCapability string

// NodeCapabilityPrefix is a prefix for [NodeCapMap] keys that share a common
// namespace, where each entry represents a distinct named instance (e.g. one
// per service). The full key is formed by concatenating the prefix with the
// instance name.
type NodeCapabilityPrefix string

// ToAttribute returns the full [NodeCapability] key for the given value under
// this prefix, of the form prefix+value.
func (p NodeCapabilityPrefix) ToAttribute(value string) NodeCapability {
	return NodeCapability(string(p) + value)
}

const (
	CapabilityFileSharing        NodeCapability = "https://tailscale.com/cap/file-sharing"
	CapabilityAdmin              NodeCapability = "https://tailscale.com/cap/is-admin"
	CapabilityOwner              NodeCapability = "https://tailscale.com/cap/is-owner"
	CapabilitySSH                NodeCapability = "https://tailscale.com/cap/ssh"                   // feature enabled/available
	CapabilitySSHRuleIn          NodeCapability = "https://tailscale.com/cap/ssh-rule-in"           // some SSH rule reach this node
	CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
	CapabilityDebug              NodeCapability = "https://tailscale.com/cap/debug"                 // exposes debug endpoints over the PeerAPI
	CapabilityHTTPS              NodeCapability = "https"

	// CapabilityMacUIV2 makes the macOS GUI enable its v2 mode.
	CapabilityMacUIV2 NodeCapability = "https://tailscale.com/cap/mac-ui-v2"

	// CapabilityBindToInterfaceByRoute changes how Darwin nodes create
	// sockets (in the net/netns package). See that package for more
	// details on the behaviour of this capability.
	CapabilityBindToInterfaceByRoute NodeCapability = "https://tailscale.com/cap/bind-to-interface-by-route"

	// NodeAttrDisableAndroidBindToActiveNetwork disables binding sockets to the
	// currently active network on Android, which is enabled by default.
	// This allows the control plane to turn off the behavior if it causes
	// problems.
	NodeAttrDisableAndroidBindToActiveNetwork NodeCapability = "disable-android-bind-to-active-network"

	// CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin
	// nodes get the default interface. There is an optional hook (used by the
	// macOS and iOS clients) to override the default interface, this capability
	// disables that and uses the default behavior (of parsing the routing
	// table).
	CapabilityDebugDisableAlternateDefaultRouteInterface NodeCapability = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface"

	// CapabilityDebugDisableBindConnToInterface disables the automatic binding
	// of connections to the default network interface on Darwin nodes.
	CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface"

	// CapabilityDebugDisableBindConnToInterface disables the automatic binding
	// of connections to the default network interface on Darwin nodes using network extensions
	CapabilityDebugDisableBindConnToInterfaceAppleExt NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext"

	// CapabilityTailnetLock indicates the node may initialize tailnet lock.
	CapabilityTailnetLock NodeCapability = "https://tailscale.com/cap/tailnet-lock"

	// Funnel warning capabilities used for reporting errors to the user.

	// CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
	// This cap is no longer used 2023-08-09 onwards.
	CapabilityWarnFunnelNoInvite NodeCapability = "https://tailscale.com/cap/warn-funnel-no-invite"

	// CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
	// This cap is no longer used 2023-08-09 onwards.
	CapabilityWarnFunnelNoHTTPS NodeCapability = "https://tailscale.com/cap/warn-funnel-no-https"

	// Debug logging capabilities

	// CapabilityDebugTSDNSResolution enables verbose debug logging for DNS
	// resolution for Tailscale-controlled domains (the control server, log
	// server, DERP servers, etc.)
	CapabilityDebugTSDNSResolution NodeCapability = "https://tailscale.com/cap/debug-ts-dns-resolution"

	// CapabilityFunnelPorts specifies the ports that the Funnel is available on.
	// The ports are specified as a comma-separated list of port numbers or port
	// ranges (e.g. "80,443,8080-8090") in the ports query parameter.
	// e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090
	CapabilityFunnelPorts NodeCapability = "https://tailscale.com/cap/funnel-ports"

	// NodeAttrOnlyTCP443 specifies that the client should not attempt to generate
	// any outbound traffic that isn't TCP on port 443 (HTTPS). This is used for
	// clients in restricted environments where only HTTPS traffic is allowed
	// other types of traffic trips outbound firewall alarms. This thus implies
	// all traffic is over DERP.
	NodeAttrOnlyTCP443 NodeCapability = "only-tcp-443"

	// NodeAttrFunnel grants the ability for a node to host ingress traffic.
	NodeAttrFunnel NodeCapability = "funnel"
	// NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions.
	NodeAttrSSHAggregator NodeCapability = "ssh-aggregator"

	// NodeAttrDebugForceBackgroundSTUN forces a node to always do background
	// STUN queries regardless of inactivity.
	NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun"

	// NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration,
	// always giving WireGuard the full netmap, even for idle peers.
	NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim"

	// NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be
	// disabled if WPAD is present on the network.
	NodeAttrDisableSubnetsIfPAC NodeCapability = "debug-disable-subnets-if-pac"

	// NodeAttrDisableUPnP makes the client not perform a UPnP portmapping.
	// By default, we want to enable it to see if it works on more clients.
	//
	// If UPnP catastrophically fails for people, this should be set kill
	// new attempts at UPnP connections.
	NodeAttrDisableUPnP NodeCapability = "debug-disable-upnp"

	// NodeAttrDisableDeltaUpdates makes the client not process updates via the
	// delta update mechanism and should instead treat all netmap changes as
	// "full" ones as tailscaled did in 1.48.x and earlier.
	NodeAttrDisableDeltaUpdates NodeCapability = "disable-delta-updates"

	// NodeAttrRandomizeClientPort makes magicsock UDP bind to
	// :0 to get a random local port, ignoring any configured
	// fixed port.
	NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port"

	// NodeAttrSilentDisco makes the client suppress disco heartbeats to its
	// peers.
	NodeAttrSilentDisco NodeCapability = "silent-disco"

	// NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route
	// rather than a /32 per peer. At most one of this or
	// NodeAttrOneCGNATDisable may be set; if neither are, it's automatic.
	NodeAttrOneCGNATEnable NodeCapability = "one-cgnat?v=true"

	// NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer
	// rather than one big /10 CGNAT route. At most one of this or
	// NodeAttrOneCGNATEnable may be set; if neither are, it's automatic.
	NodeAttrOneCGNATDisable NodeCapability = "one-cgnat?v=false"

	// NodeAttrPeerMTUEnable makes the client do path MTU discovery to its
	// peers. If it isn't set, it defaults to the client default.
	NodeAttrPeerMTUEnable NodeCapability = "peer-mtu-enable"

	// NodeAttrDNSForwarderDisableTCPRetries disables retrying truncated
	// DNS queries over TCP if the response is truncated.
	NodeAttrDNSForwarderDisableTCPRetries NodeCapability = "dns-forwarder-disable-tcp-retries"

	// NodeAttrLinuxMustUseIPTables forces Linux clients to use iptables for
	// netfilter management.
	// This cannot be set simultaneously with NodeAttrLinuxMustUseNfTables.
	NodeAttrLinuxMustUseIPTables NodeCapability = "linux-netfilter?v=iptables"

	// NodeAttrLinuxMustUseNfTables forces Linux clients to use nftables for
	// netfilter management.
	// This cannot be set simultaneously with NodeAttrLinuxMustUseIPTables.
	NodeAttrLinuxMustUseNfTables NodeCapability = "linux-netfilter?v=nftables"

	// NodeAttrDisableSeamlessKeyRenewal disables seamless key renewal, which is
	// enabled by default in clients as of 2025-09-17 (1.90 and later).
	//
	// We will use this attribute to manage the rollout, and disable seamless in
	// clients with known bugs.
	// http://go/seamless-key-renewal
	NodeAttrDisableSeamlessKeyRenewal NodeCapability = "disable-seamless-key-renewal"

	// NodeAttrSeamlessKeyRenewal was used to opt-in to seamless key renewal
	// during its private alpha.
	//
	// Deprecated: NodeAttrSeamlessKeyRenewal is deprecated as of CapabilityVersion 126,
	// because seamless key renewal is now enabled by default.
	NodeAttrSeamlessKeyRenewal NodeCapability = "seamless-key-renewal"

	// NodeAttrProbeUDPLifetime makes the client probe UDP path lifetime at the
	// tail end of an active direct connection in magicsock.
	NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime"

	// NodeAttrsTaildriveShare enables sharing via Taildrive.
	NodeAttrsTaildriveShare NodeCapability = "drive:share"

	// NodeAttrsTaildriveAccess enables accessing shares via Taildrive.
	NodeAttrsTaildriveAccess NodeCapability = "drive:access"

	// NodeAttrSuggestExitNode is applied to each exit node which the control plane has determined
	// is a recommended exit node.
	NodeAttrSuggestExitNode NodeCapability = "suggest-exit-node"

	// NodeAttrDisableWebClient disables using the web client.
	NodeAttrDisableWebClient NodeCapability = "disable-web-client"

	// NodeAttrLogExitFlows enables exit node destinations in network flow logs.
	NodeAttrLogExitFlows NodeCapability = "log-exit-flows"

	// NodeAttrAutoExitNode permits the automatic exit nodes feature.
	NodeAttrAutoExitNode NodeCapability = "auto-exit-node"

	// NodeAttrStoreAppCRoutes configures the node to store app connector routes persistently.
	NodeAttrStoreAppCRoutes NodeCapability = "store-appc-routes"

	// NodeAttrSuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI.
	NodeAttrSuggestExitNodeUI NodeCapability = "suggest-exit-node-ui"

	// NodeAttrUserDialUseRoutes makes UserDial use either the peer dialer or the system dialer,
	// depending on the destination address and the configured routes. When present, it also makes
	// the DNS forwarder use UserDial instead of SystemDial when dialing resolvers.
	NodeAttrUserDialUseRoutes NodeCapability = "user-dial-routes"

	// NodeAttrSSHBehaviorV1 forces SSH to use the V1 behavior (no su, run SFTP in-process)
	// Added 2024-05-29 in Tailscale version 1.68.
	NodeAttrSSHBehaviorV1 NodeCapability = "ssh-behavior-v1"

	// NodeAttrSSHBehaviorV2 forces SSH to use the V2 behavior (use su, run SFTP in child process).
	// This overrides NodeAttrSSHBehaviorV1 if set.
	// See forceV1Behavior in ssh/tailssh/incubator.go for distinction between
	// V1 and V2 behavior.
	// Added 2024-08-06 in Tailscale version 1.72.
	NodeAttrSSHBehaviorV2 NodeCapability = "ssh-behavior-v2"

	// NodeAttrDisableSplitDNSWhenNoCustomResolvers indicates that the node's
	// DNS manager should not adopt a split DNS configuration even though the
	// Config of the resolver only contains routes that do not specify custom
	// resolver(s), hence all DNS queries can be safely sent to the upstream
	// DNS resolver and the node's DNS forwarder doesn't need to handle all
	// DNS traffic.
	// This is for now (2024-06-06) an iOS-specific battery life optimization,
	// and this node attribute allows us to disable the optimization remotely
	// if needed.
	NodeAttrDisableSplitDNSWhenNoCustomResolvers NodeCapability = "disable-split-dns-when-no-custom-resolvers"

	// NodeAttrDisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not
	// create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled.
	// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
	// associated with all network adapters, even when "Override local DNS" is enabled and/or
	// a Mullvad exit node is being used, resulting in DNS leaks.
	// We began creating this rule on 2024-06-14, and this node attribute
	// allows us to disable the new behavior remotely if needed.
	NodeAttrDisableLocalDNSOverrideViaNRPT NodeCapability = "disable-local-dns-override-via-nrpt"

	// NodeAttrDisableMagicSockCryptoRouting disables the use of the
	// magicsock cryptorouting hook. See tailscale/corp#20732.
	//
	// Deprecated: NodeAttrDisableMagicSockCryptoRouting is deprecated as of
	// CapabilityVersion 124, CryptoRouting is now mandatory. See tailscale/corp#31083.
	NodeAttrDisableMagicSockCryptoRouting NodeCapability = "disable-magicsock-crypto-routing"

	// NodeAttrDisableCaptivePortalDetection instructs the client to not perform captive portal detection
	// automatically when the network state changes.
	NodeAttrDisableCaptivePortalDetection NodeCapability = "disable-captive-portal-detection"

	// NodeAttrDisableSkipStatusQueue is set when the node should disable skipping
	// of queued netmap.NetworkMap between the controlclient and LocalBackend.
	// See tailscale/tailscale#14768.
	NodeAttrDisableSkipStatusQueue NodeCapability = "disable-skip-status-queue"

	// NodeAttrSSHEnvironmentVariables enables logic for handling environment variables sent
	// via SendEnv in the SSH server and applying them to the SSH session.
	NodeAttrSSHEnvironmentVariables NodeCapability = "ssh-env-vars"

	// NodeAttrServiceHost indicates the VIP Services for which the client is
	// approved to act as a service host, and which IP addresses are assigned
	// to those VIP Services. Any VIP Services that the client is not
	// advertising can be ignored.
	// Each value of this key in [NodeCapMap] is of type [ServiceIPMappings].
	// If multiple values of this key exist, they should be merged in sequence
	// (replace conflicting keys).
	NodeAttrServiceHost NodeCapability = "service-host"

	// NodeAttrMaxKeyDuration represents the MaxKeyDuration setting on the
	// tailnet. The value of this key in [NodeCapMap] will be only one entry of
	// type float64 representing the duration in seconds. This cap will be
	// omitted if the tailnet's MaxKeyDuration is the default.
	NodeAttrMaxKeyDuration NodeCapability = "tailnet.maxKeyDuration"

	// NodeAttrNativeIPV4 contains the IPV4 address of the node in its
	// native tailnet. This is currently only sent to Hello, in its
	// peer node list.
	NodeAttrNativeIPV4 NodeCapability = "native-ipv4"

	// NodeAttrDisableRelayServer prevents the node from acting as an underlay
	// UDP relay server. There are no expected values for this key; the key
	// only needs to be present in [NodeCapMap] to take effect.
	NodeAttrDisableRelayServer NodeCapability = "disable-relay-server"

	// NodeAttrDisableRelayClient prevents the node from both allocating UDP
	// relay server endpoints itself, and from using endpoints allocated by
	// its peers. This attribute can be added to the node dynamically; if added
	// while the node is already running, the node will be unable to allocate
	// endpoints after it next updates its network map, and will be immediately
	// unable to use new paths via a UDP relay server. Setting this attribute
	// dynamically does not remove any existing paths, including paths that
	// traverse a UDP relay server. There are no expected values for this key
	// in [NodeCapMap]; the key only needs to be present in [NodeCapMap] to
	// take effect.
	NodeAttrDisableRelayClient NodeCapability = "disable-relay-client"

	// NodeAttrMagicDNSPeerAAAA is a capability that tells the node's MagicDNS
	// server to answer AAAA queries about its peers. See tailscale/tailscale#1152.
	NodeAttrMagicDNSPeerAAAA NodeCapability = "magicdns-aaaa"

	// NodeAttrDNSSubdomainResolve, when set on Self or a Peer node, indicates
	// that the subdomains of that node's MagicDNS name should resolve to the
	// same IP addresses as the node itself.
	// For example, if node "myserver.tailnet.ts.net" has this capability,
	// then "anything.myserver.tailnet.ts.net" will resolve to myserver's IPs.
	NodeAttrDNSSubdomainResolve NodeCapability = "dns-subdomain-resolve"

	// NodeAttrTrafficSteering configures the node to use the traffic
	// steering subsystem for via routes. See tailscale/corp#29966.
	NodeAttrTrafficSteering NodeCapability = "traffic-steering"

	// NodeAttrTailnetDisplayName is an optional alternate name for the tailnet
	// to be displayed to the user.
	// If empty or absent, a default is used.
	// If this value is present and set by a user this will only include letters,
	// numbers, apostrophe, spaces, and hyphens. This may not be true for the default.
	// Values can look like "foo.com" or "Foo's Test Tailnet - Staging".
	NodeAttrTailnetDisplayName NodeCapability = "tailnet-display-name"

	// NodeAttrClientSideReachability configures the node to determine
	// reachability itself when choosing connectors. When absent, the
	// default behavior is to trust the control plane when it claims that a
	// node is no longer online, but that is not a reliable signal.
	NodeAttrClientSideReachability = "client-side-reachability"

	// NodeAttrDefaultAutoUpdate advertises the default node auto-update setting
	// for this tailnet. The node is free to opt-in or out locally regardless of
	// this value. Once this has been set and stored in the client, future
	// changes from the control plane are ignored.
	//
	// The value of the key in [NodeCapMap] is a JSON boolean.
	NodeAttrDefaultAutoUpdate NodeCapability = "default-auto-update"

	// NodeAttrDisableHostsFileUpdates indicates that the node's DNS manager should
	// not create hosts file entries when it normally would, such as when we're not
	// the primary resolver on Windows or when the host is domain-joined and its
	// primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only
	// used on Windows.
	NodeAttrDisableHostsFileUpdates NodeCapability = "disable-hosts-file-updates"

	// NodeAttrForceRegisterMagicDNSIPv4Only forces the client to only register
	// its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses.
	// See https://github.com/tailscale/tailscale/issues/15404.
	// TODO(bradfitz): remove this a few releases after 2026-02-16.
	NodeAttrForceRegisterMagicDNSIPv4Only NodeCapability = "force-register-magicdns-ipv4-only"

	// NodeAttrCacheNetworkMaps instructs the node to persistently cache network
	// maps and use them to establish peer connectivity on start, if doing so is
	// supported by the client and storage is available. When this attribute is
	// absent (or removed), a node that supports netmap caching will ignore and
	// discard existing cached maps, and will not store any.
	NodeAttrCacheNetworkMaps NodeCapability = "cache-network-maps"

	// NodeAttrDisableLinuxCGNATDropRule tells Linux clients to not insert a
	// blanket firewall DROP rule for inbound traffic from the CGNAT IP range
	// that does not originate from the Tailscale network interface.
	// This enables access to off-tailnet endpoints within that IP range.
	NodeAttrDisableLinuxCGNATDropRule NodeCapability = "disable-linux-cgnat-drop-rule"
)

const (
	// NodeAttrPrefixServices is the prefix for per-service [NodeCapMap]
	// entries describing Services visible (accessible) to this node.
	// Each value under such a key is of type [ServiceDetails].
	// The suffix after the prefix is an opaque server-chosen identifier;
	// consumers must use [ServiceDetails.Name] as the canonical service name
	// rather than parsing it from the map key.
	NodeAttrPrefixServices NodeCapabilityPrefix = "services/"
)

// SetDNSRequest is a request to add a DNS record.
//
// This is used to let tailscaled clients complete their ACME DNS-01 challenges
// (so people can use LetsEncrypt, etc) to get TLS certificates for
// their foo.bar.ts.net MagicDNS names.
//
// This is JSON-encoded and sent over the control plane connection to:
//
//	POST https://<control-plane>/machine/set-dns
type SetDNSRequest struct {
	// Version is the client's capabilities
	// (CurrentCapabilityVersion) when using the Noise transport.
	//
	// When using the original nacl crypto_box transport, the
	// value must be 1.
	Version CapabilityVersion

	// NodeKey is the client's current node key.
	NodeKey key.NodePublic

	// Name is the domain name for which to create a record.
	// For ACME DNS-01 challenges, it should be one of the domains
	// in MapResponse.DNSConfig.CertDomains with the prefix
	// "_acme-challenge.".
	Name string

	// Type is the DNS record type. For ACME DNS-01 challenges, it
	// should be "TXT".
	Type string

	// Value is the value to add.
	Value string
}

// SetDNSResponse is the response to a SetDNSRequest.
type SetDNSResponse struct{}

// HealthChangeRequest is the JSON request body type used to report
// node health changes to:
//
//	POST https://<control-plane>/machine/update-health.
//
// As of 2025-10-02, we stopped sending this to the control plane proactively.
// It was never useful enough with its current design and needs more thought.
type HealthChangeRequest struct {
	Subsys string // a health.Subsystem value in string form
	Error  string // or empty if cleared

	// NodeKey is the client's current node key.
	// In clients <= 1.62.0 it was always the zero value.
	NodeKey key.NodePublic
}

// SetDeviceAttributesRequest is a request to update the
// current node's device posture attributes.
//
// As of 2024-12-30, this is an experimental dev feature
// for internal testing. See tailscale/corp#24690.
//
// This is JSON-encoded and sent over the control plane connection to:
//
//	PATCH https://<control-plane>/machine/set-device-attr
type SetDeviceAttributesRequest struct {
	// Version is the current binary's [CurrentCapabilityVersion].
	Version CapabilityVersion

	// NodeKey identifies the node to modify. It should be the currently active
	// node and is an error if not.
	NodeKey key.NodePublic

	// Update is a map of device posture attributes to update.
	// Attributes not in the map are left unchanged.
	Update AttrUpdate
}

// AttrUpdate is a map of attributes to update.
// Attributes not in the map are left unchanged.
// The value can be a string, float64, bool, or nil to delete.
//
// See https://tailscale.com/s/api-device-posture-attrs.
//
// TODO(bradfitz): add struct type for specifying optional associated data
// for each attribute value, like an expiry time?
type AttrUpdate map[string]any

// SSHPolicy is the policy for how to handle incoming SSH connections
// over Tailscale.
type SSHPolicy struct {
	// Rules are the rules to process for an incoming SSH connection. The first
	// matching rule takes its action and stops processing further rules.
	//
	// When an incoming connection first starts, all rules are evaluated in
	// "none" auth mode, where the client hasn't even been asked to send a
	// public key. All SSHRule.Principals requiring a public key won't match. If
	// a rule matches on the first pass and its Action is reject, the
	// authentication fails with that action's rejection message, if any.
	//
	// If the first pass rule evaluation matches nothing without matching an
	// Action with Reject set, the rules are considered to see whether public
	// keys might still result in a match. If not, "none" auth is terminated
	// before proceeding to public key mode. If so, the client is asked to try
	// public key authentication and the rules are evaluated again for each of
	// the client's present keys.
	Rules []*SSHRule `json:"rules"`
}

// An SSH rule is a match predicate and associated action for an incoming SSH connection.
type SSHRule struct {
	// RuleExpires, if non-nil, is when this rule expires.
	//
	// For example, a (principal,sshuser) tuple might be granted
	// prompt-free SSH access for N minutes, so this rule would be
	// before a expiration-free rule for the same principal that
	// required an auth prompt.  This permits the control plane to
	// be out of the path for already-authorized SSH pairs.
	//
	// Once a rule matches, the lifetime of any accepting connection
	// is subject to the SSHAction.SessionExpires time, if any.
	RuleExpires *time.Time `json:"ruleExpires,omitempty"`

	// Principals matches an incoming connection. If the connection
	// matches anything in this list and also matches SSHUsers,
	// then Action is applied.
	Principals []*SSHPrincipal `json:"principals"`

	// SSHUsers are the SSH users that this rule matches. It is a
	// map from either ssh-user|"*" => local-user.  The map must
	// contain a key for either ssh-user or, as a fallback, "*" to
	// match anything. If it does, the map entry's value is the
	// actual user that's logged in.
	// If the map value is the empty string (for either the
	// requested SSH user or "*"), the rule doesn't match.
	// If the map value is "=", it means the ssh-user should map
	// directly to the local-user.
	// It may be nil if the Action is reject.
	SSHUsers map[string]string `json:"sshUsers"`

	// Action is the outcome to task.
	// A nil or invalid action means to deny.
	Action *SSHAction `json:"action"`

	// AcceptEnv is a slice of environment variable names that are allowlisted
	// for the SSH rule in the policy file.
	//
	// AcceptEnv values may contain * and ? wildcard characters which match against
	// an arbitrary number of characters or a single character respectively.
	AcceptEnv []string `json:"acceptEnv,omitempty"`
}

// SSHPrincipal is either a particular node or a user on any node.
type SSHPrincipal struct {
	// Matching any one of the following four field causes a match.
	// It must also match Certs, if non-empty.

	Node      StableNodeID `json:"node,omitempty"`
	NodeIP    string       `json:"nodeIP,omitempty"`
	UserLogin string       `json:"userLogin,omitempty"` // email-ish: foo@example.com, bar@github
	Any       bool         `json:"any,omitempty"`       // if true, match any connection
	// TODO(bradfitz): add StableUserID, once that exists

	// UnusedPubKeys was public key support. It never became an official product
	// feature and so as of 2024-12-12 is being removed.
	// This stub exists to remind us not to re-use the JSON field name "pubKeys"
	// in the future if we bring it back with different semantics.
	//
	// Deprecated: do not use. It does nothing.
	UnusedPubKeys []string `json:"pubKeys,omitempty"`
}

// SSHAction is how to handle an incoming connection.
// At most one field should be non-zero.
type SSHAction struct {
	// Message, if non-empty, is shown to the user before the
	// action occurs.
	Message string `json:"message,omitempty"`

	// Reject, if true, terminates the connection. This action
	// has higher priority that Accept, if given.
	// The reason this is exists is primarily so a response
	// from HoldAndDelegate has a way to stop the poll.
	Reject bool `json:"reject,omitempty"`

	// Accept, if true, accepts the connection immediately
	// without further prompts.
	Accept bool `json:"accept,omitempty"`

	// SessionDuration, if non-zero, is how long the session can stay open
	// before being forcefully terminated.
	SessionDuration time.Duration `json:"sessionDuration,omitempty,format:nano"`

	// AllowAgentForwarding, if true, allows accepted connections to forward
	// the ssh agent if requested.
	AllowAgentForwarding bool `json:"allowAgentForwarding,omitempty"`

	// HoldAndDelegate, if non-empty, is a URL that serves an
	// outcome verdict.  The connection will be accepted and will
	// block until the provided long-polling URL serves a new
	// SSHAction JSON value. The URL must be fetched using the
	// Noise transport (in package control/control{base,http}).
	// If the long poll breaks before returning a complete HTTP
	// response, it should be re-fetched as long as the SSH
	// session is open.
	//
	// The following variables in the URL are expanded by tailscaled:
	//
	//   * $SRC_NODE_IP (URL escaped)
	//   * $SRC_NODE_ID (Node.ID as int64 string)
	//   * $DST_NODE_IP (URL escaped)
	//   * $DST_NODE_ID (Node.ID as int64 string)
	//   * $SSH_USER (URL escaped, ssh user requested)
	//   * $LOCAL_USER (URL escaped, local user mapped)
	HoldAndDelegate string `json:"holdAndDelegate,omitempty"`

	// AllowLocalPortForwarding, if true, allows accepted connections
	// to use local port forwarding if requested.
	AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"`

	// AllowRemotePortForwarding, if true, allows accepted connections
	// to use remote port forwarding if requested.
	AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"`

	// Recorders defines the destinations of the SSH session recorders.
	// The recording will be uploaded to http://addr:port/record.
	Recorders []netip.AddrPort `json:"recorders,omitempty"`

	// OnRecorderFailure is the action to take if recording fails.
	// If nil, the default action is to fail open.
	OnRecordingFailure *SSHRecorderFailureAction `json:"onRecordingFailure,omitempty"`
}

// SSHRecorderFailureAction is the action to take if recording fails.
type SSHRecorderFailureAction struct {
	// RejectSessionWithMessage, if not empty, specifies that the session should
	// be rejected if the recording fails to start.
	// The message will be shown to the user before the session is rejected.
	RejectSessionWithMessage string `json:",omitempty"`

	// TerminateSessionWithMessage, if not empty, specifies that the session
	// should be terminated if the recording fails after it has started. The
	// message will be shown to the user before the session is terminated.
	TerminateSessionWithMessage string `json:",omitempty"`

	// NotifyURL, if non-empty, specifies a HTTP POST URL to notify when the
	// recording fails. The payload is the JSON encoded
	// SSHRecordingFailureNotifyRequest struct. The host field in the URL is
	// ignored, and it will be sent to control over the Noise transport.
	NotifyURL string `json:",omitempty"`
}

// SSHEventNotifyRequest is the JSON payload sent to the NotifyURL
// for an SSH event.
//
//	POST https://<control-plane>/[...varies, sent in SSH policy...]
type SSHEventNotifyRequest struct {
	// EventType is the type of notify request being sent.
	EventType SSHEventType

	// ConnectionID uniquely identifies a connection made to the SSH server.
	// It may be shared across multiple sessions over the same connection in
	// case a single connection creates multiple sessions.
	ConnectionID string

	// CapVersion is the client's current CapabilityVersion.
	CapVersion CapabilityVersion

	// NodeKey is the client's current node key.
	NodeKey key.NodePublic

	// SrcNode is the ID of the node that initiated the SSH session.
	SrcNode NodeID

	// SSHUser is the user that was presented to the SSH server.
	SSHUser string

	// LocalUser is the user that was resolved from the SSHUser for the local machine.
	LocalUser string

	// RecordingAttempts is the list of recorders that were attempted, in order.
	RecordingAttempts []*SSHRecordingAttempt
}

// SSHEventType defines the event type linked to a SSH action or state.
type SSHEventType int

const (
	UnspecifiedSSHEventType SSHEventType = 0
	// SSHSessionRecordingRejected is the event that
	// defines when a SSH session cannot be started
	// because no recorder is available for session
	// recording, and the SSHRecorderFailureAction
	// RejectSessionWithMessage is not empty.
	SSHSessionRecordingRejected SSHEventType = 1
	// SSHSessionRecordingTerminated is the event that
	// defines when session recording has failed
	// during the session and the SSHRecorderFailureAction
	// TerminateSessionWithMessage is not empty.
	SSHSessionRecordingTerminated SSHEventType = 2
	// SSHSessionRecordingFailed is the event that
	// defines when session recording is unavailable and
	// the SSHRecorderFailureAction RejectSessionWithMessage
	// or TerminateSessionWithMessage is empty.
	SSHSessionRecordingFailed SSHEventType = 3
)

// SSHRecordingAttempt is a single attempt to start a recording.
type SSHRecordingAttempt struct {
	// Recorder is the address of the recorder that was attempted.
	Recorder netip.AddrPort

	// FailureMessage is the error message of the failed attempt.
	FailureMessage string
}

// QueryFeatureRequest is a request sent to "POST /machine/feature/query" to get
// instructions on how to enable a feature, such as Funnel, for the node's
// tailnet.
//
// See QueryFeatureResponse for response structure.
type QueryFeatureRequest struct {
	// Feature is the string identifier for a feature.
	Feature string `json:",omitzero"`
	// NodeKey is the client's current node key.
	NodeKey key.NodePublic `json:",omitzero"`
}

// QueryFeatureResponse is the response to an QueryFeatureRequest.
// See cli.enableFeatureInteractive for usage.
type QueryFeatureResponse struct {
	// Complete is true when the feature is already enabled.
	Complete bool `json:",omitzero"`

	// Text holds lines to display in the CLI with information
	// about the feature and how to enable it.
	//
	// Lines are separated by newline characters. The final
	// newline may be omitted.
	Text string `json:",omitzero"`

	// URL is the link for the user to visit to take action on
	// enabling the feature.
	//
	// When empty, there is no action for this user to take.
	URL string `json:",omitzero"`

	// ShouldWait specifies whether the CLI should block and
	// wait for the user to enable the feature.
	//
	// If this is true, the enablement from the control server
	// is expected to be a quick and uninterrupted process for
	// the user, and blocking allows them to immediately start
	// using the feature once enabled without rerunning the
	// command (e.g. no need to re-run "funnel on").
	//
	// The CLI can watch the IPN notification bus for changes in
	// required node capabilities to know when to continue.
	ShouldWait bool `json:",omitzero"`
}

// WebClientAuthResponse is the response to a web client authentication request
// sent to "/machine/webclient/action" or "/machine/webclient/wait".
// See client/web for usage.
type WebClientAuthResponse struct {
	// ID is a unique identifier for the session auth request.
	// It can be supplied to "/machine/webclient/wait" to pause until
	// the session authentication has been completed.
	ID string `json:",omitzero"`

	// URL is the link for the user to visit to authenticate the session.
	//
	// When empty, there is no action for the user to take.
	URL string `json:",omitzero"`

	// Complete is true when the session authentication has been completed.
	Complete bool `json:",omitzero"`
}

// OverTLSPublicKeyResponse is the JSON response to /key?v=<n>
// over HTTPS (regular TLS) to the Tailscale control plane server,
// where the 'v' argument is the client's current capability version
// (previously known as the "MapRequest version").
//
// The "OverTLS" prefix is to loudly declare that this exchange
// doesn't happen over Noise and can be intercepted/MITM'ed by
// enterprise/corp proxies where the organization can put TLS roots
// on devices.
type OverTLSPublicKeyResponse struct {
	// LegacyPublic specifies the control plane server's original
	// NaCl crypto_box machine key.
	// It will be zero for sufficiently new clients, based on their
	// advertised "v" parameter (the CurrentMapRequestVersion).
	// In that case, only the newer Noise-based transport may be used
	// using the PublicKey field.
	LegacyPublicKey key.MachinePublic `json:"legacyPublicKey"`

	// PublicKey specifies the server's public key for the
	// Noise-based control plane protocol. (see packages
	// control/controlbase and control/controlhttp)
	PublicKey key.MachinePublic `json:"publicKey"`
}

// TokenRequest is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.
//
// It is JSON-encoded and sent over Noise to "POST /machine/id-token".
type TokenRequest struct {
	// CapVersion is the client's current CapabilityVersion.
	CapVersion CapabilityVersion
	// NodeKey is the client's current node key.
	NodeKey key.NodePublic
	// Audience the token is being requested for.
	Audience string
}

// TokenResponse is the response to a TokenRequest.
type TokenResponse struct {
	// IDToken is a JWT encoding the following standard claims:
	//
	//   `sub` | the MagicDNS name of the node
	//   `aud` | Audience from the request
	//   `exp` | Token expiry
	//   `iat` | Token issuance time
	//   `iss` | Issuer
	//   `jti` | Random token identifier
	//   `nbf` | Not before time
	//
	// It also encodes the following Tailscale specific claims:
	//
	//   `key`       | the node public key
	//   `addresses` | the Tailscale IPs of the node
	//   `nid`       | the node ID
	//   `node`      | the name of the node
	//   `domain`    | the domain of the node, it has the same format as MapResponse.Domain.
	//   `tags`      | an array of <domain:tag> on the node (like alice.github:tag:foo or example.com:tag:foo)
	//   `user`      | user emailish (like alice.github:alice@github or example.com:bob@example.com), if not tagged
	//   `uid`       | user ID, if not tagged
	IDToken string `json:"id_token"`
}

// PeerChange is an update to a node.
type PeerChange struct {
	// NodeID is the node ID being mutated. If the NodeID is not
	// known in the current netmap, this update should be
	// ignored. (But the server will try not to send such useless
	// updates.)
	NodeID NodeID

	// DERPRegion, if non-zero, means that NodeID's home DERP
	// region ID is now this number.
	DERPRegion int `json:",omitzero"`

	// Cap, if non-zero, means that NodeID's capability version has changed.
	Cap CapabilityVersion `json:",omitzero"`

	// CapMap, if non-nil, means that NodeID's capability map has changed.
	CapMap NodeCapMap `json:",omitempty"`

	// Endpoints, if non-empty, means that NodeID's UDP Endpoints
	// have changed to these.
	Endpoints []netip.AddrPort `json:",omitempty"`

	// Key, if non-nil, means that the NodeID's wireguard public key changed.
	Key *key.NodePublic `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978

	// KeySignature, if non-nil, means that the signature of the wireguard
	// public key has changed.
	KeySignature tkatype.MarshaledSignature `json:",omitempty"`

	// DiscoKey, if non-nil, means that the NodeID's discokey changed.
	DiscoKey *key.DiscoPublic `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978

	// Online, if non-nil, means that the NodeID's online status changed.
	Online *bool `json:",omitzero"`

	// LastSeen, if non-nil, means that the NodeID's online status changed.
	LastSeen *time.Time `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978

	// KeyExpiry, if non-nil, changes the NodeID's key expiry.
	KeyExpiry *time.Time `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
}

// DerpMagicIP is a fake WireGuard endpoint IP address that means to
// use DERP. When used (in the Node.DERP field), the port number of
// the WireGuard endpoint is the DERP region ID number to use.
//
// Mnemonic: 3.3.40 are numbers above the keys D, E, R, P.
const DerpMagicIP = "127.3.3.40"

var DerpMagicIPAddr = netip.MustParseAddr(DerpMagicIP)

// EarlyNoise is the early payload that's sent over Noise but before the HTTP/2
// handshake when connecting to the coordination server.
//
// This exists to let the server push some early info to client for that
// stateful HTTP/2+Noise connection without incurring an extra round trip. (This
// would've used HTTP/2 server push, had Go's client-side APIs been available)
type EarlyNoise struct {
	// NodeKeyChallenge is a random per-connection public key to be used by
	// the client to prove possession of a wireguard private key.
	NodeKeyChallenge key.ChallengePublic `json:"nodeKeyChallenge"`
}

// LBHeader is the HTTP request header used to provide a load balancer or
// internal reverse proxy with information about the request body without the
// reverse proxy needing to read the body to parse it out. Think of it akin to
// an HTTP Host header or SNI. The value may be absent (notably for old clients)
// but if present, it should match the request. A non-empty value that doesn't
// match the request body's.
//
// The possible values depend on the request path, but for /machine (Noise)
// requests, they'll usually be a node public key (in key.NodePublic.String
// format), matching the Request JSON body's NodeKey.
//
// Note that this is not a security or authentication header; it's strictly
// denormalized redundant data as an optimization.
//
// For some request types, the header may have multiple values. (e.g. OldNodeKey
// vs NodeKey)
const LBHeader = "Ts-Lb"

// ServiceIPMappings maps ServiceName to lists of IP addresses. This is used
// as the value of the [NodeAttrServiceHost] capability, to inform service hosts
// what IP addresses they need to listen on for each service that they are
// advertising.
//
// This is of the form:
//
//	{
//	  "svc:samba": ["100.65.32.1", "fd7a:115c:a1e0::1234"],
//	  "svc:web": ["100.102.42.3", "fd7a:115c:a1e0::abcd"],
//	}
//
// where the IP addresses are the IPs of the VIP services. These IPs are also
// provided in AllowedIPs, but this lets the client know which services
// correspond to those IPs. Any services that don't correspond to a service
// this client is hosting can be ignored.
type ServiceIPMappings map[ServiceName][]netip.Addr

// ServiceDetails describes a Service visible to this node.
// It is the value type stored under [NodeAttrPrefixServices]+serviceName keys in [NodeCapMap].
type ServiceDetails struct {
	// Name is the name of the Service, of the form "svc:dns-label".
	Name ServiceName

	// Addrs are the IP addresses (IPv4 and IPv6) assigned to this Service.
	Addrs []netip.Addr `json:",omitempty"`

	// Ports are the protocol/port combinations the Service accepts.
	Ports []ProtoPortRange `json:",omitempty"`
}

// ClientAuditAction represents an auditable action that a client can report to the
// control plane.  These actions must correspond to the supported actions
// in the control plane.
type ClientAuditAction string

const (
	// AuditNodeDisconnect action is sent when a node has disconnected
	// from the control plane.  The details must include a reason in the Details
	// field, either generated, or entered by the user.
	AuditNodeDisconnect = ClientAuditAction("DISCONNECT_NODE")
)

// AuditLogRequest represents an audit log request to be sent to the control plane.
//
// This is JSON-encoded and sent over the control plane connection to:
// POST https://<control-plane>/machine/audit-log
type AuditLogRequest struct {
	// Version is the client's current CapabilityVersion.
	Version CapabilityVersion `json:",omitzero"`
	// NodeKey is the client's current node key.
	NodeKey key.NodePublic `json:",omitzero"`
	// Action is the action to be logged. It must correspond to a known action in the control plane.
	Action ClientAuditAction `json:",omitzero"`
	// Details is an opaque string, specific to the action being logged.  Empty strings may not
	// be valid depending on the action being logged.
	Details string `json:",omitzero"`
	// Timestamp is the time at which the audit log was generated on the node.
	Timestamp time.Time `json:",omitzero"`
}