Compare commits
76 Commits
will/conta
...
irbekrm/sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350d37286d | ||
|
|
ab1eb428d9 | ||
|
|
6c3899e6ee | ||
|
|
70b7201744 | ||
|
|
44e337cc0e | ||
|
|
6b582cb8b6 | ||
|
|
24487815e1 | ||
|
|
69f5664075 | ||
|
|
3aca29e00e | ||
|
|
4d668416b8 | ||
|
|
52f16b5d10 | ||
|
|
38bba2d23a | ||
|
|
b7104cde4a | ||
|
|
7ad2bb87a6 | ||
|
|
61a1644c2a | ||
|
|
b0e96a6c39 | ||
|
|
7c0651aea6 | ||
|
|
256ecd0e8f | ||
|
|
f7acbefbbb | ||
|
|
30c9189ed3 | ||
|
|
5bd19fd3e3 | ||
|
|
f7f496025a | ||
|
|
c42a4e407a | ||
|
|
d0ef3a25df | ||
|
|
58b8f78e7e | ||
|
|
370ecb4654 | ||
|
|
c1c50cfcc0 | ||
|
|
55b372a79f | ||
|
|
87154a2f88 | ||
|
|
ddcffaef7a | ||
|
|
abab0d4197 | ||
|
|
79b547804b | ||
|
|
24bac27632 | ||
|
|
2bb837a9cf | ||
|
|
6f6383f69e | ||
|
|
7039c06d9b | ||
|
|
7c52b27daf | ||
|
|
291f91d164 | ||
|
|
c446451bfa | ||
|
|
993acf4475 | ||
|
|
2e404b769d | ||
|
|
94a4f701c2 | ||
|
|
efddad7d7d | ||
|
|
0f042b9814 | ||
|
|
6c79f55d48 | ||
|
|
1217f655c0 | ||
|
|
664b861cd4 | ||
|
|
6f0c5e0c05 | ||
|
|
128c99d4ae | ||
|
|
9f0eaa4464 | ||
|
|
78f257d9f8 | ||
|
|
5486d8aaf9 | ||
|
|
a6cc2fdc3e | ||
|
|
2404b1444e | ||
|
|
c424e192c0 | ||
|
|
2bd3c1474b | ||
|
|
5ea071186e | ||
|
|
9612001cf4 | ||
|
|
b6153efb7d | ||
|
|
653721541c | ||
|
|
8d6d9d28ba | ||
|
|
e0762fe331 | ||
|
|
0b16620b80 | ||
|
|
0f5e031133 | ||
|
|
db3776d5bf | ||
|
|
af931dcccd | ||
|
|
36efc50817 | ||
|
|
b752bde280 | ||
|
|
5595b61b96 | ||
|
|
a633a30711 | ||
|
|
60657ac83f | ||
|
|
84f8311bcd | ||
|
|
ba70cbb930 | ||
|
|
e1a4b89dbe | ||
|
|
2aeef4e610 | ||
|
|
b4b2ec7801 |
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
# Note: this is the 'v3' tag as of 2023-08-14
|
||||
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
|
||||
with:
|
||||
version: v1.54.2
|
||||
version: v1.56
|
||||
|
||||
# Show only new issues if it's a pull request.
|
||||
only-new-issues: true
|
||||
|
||||
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@@ -183,6 +183,19 @@ jobs:
|
||||
# the equals signs cause great confusion.
|
||||
run: go test ./... -bench . -benchtime 1x -run "^$"
|
||||
|
||||
privileged:
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: golang:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: chown
|
||||
run: chown -R $(id -u):$(id -g) $PWD
|
||||
- name: privileged tests
|
||||
run: ./tool/go test ./util/linuxfw
|
||||
|
||||
vm:
|
||||
runs-on: ["self-hosted", "linux", "vm"]
|
||||
# VM tests run with some privileges, don't let them run on 3p PRs.
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.21-alpine AS build-env
|
||||
FROM golang:1.22-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ not open source.
|
||||
|
||||
## Building
|
||||
|
||||
We always require the latest Go release, currently Go 1.21. (While we build
|
||||
We always require the latest Go release, currently Go 1.22. (While we build
|
||||
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
|
||||
required.)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.59.0
|
||||
1.61.0
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// RouteAdvertiser is an interface that allows the AppConnector to advertise
|
||||
@@ -51,8 +52,8 @@ type AppConnector struct {
|
||||
// mu guards the fields that follow
|
||||
mu sync.Mutex
|
||||
|
||||
// domains is a map of lower case domain names with no trailing dot, to a
|
||||
// list of resolved IP addresses.
|
||||
// domains is a map of lower case domain names with no trailing dot, to an
|
||||
// ordered list of resolved IP addresses.
|
||||
domains map[string][]netip.Addr
|
||||
|
||||
// controlRoutes is the list of routes that were last supplied by control.
|
||||
@@ -206,7 +207,16 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
nextAnswer:
|
||||
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
|
||||
// a CNAME chain back to the original query for flattening. The keys are
|
||||
// CNAME record targets, and the value is the name the record answers, so
|
||||
// for www.example.com CNAME example.com, the map would contain
|
||||
// ["example.com"] = "www.example.com".
|
||||
var cnameChain map[string]string
|
||||
|
||||
// addressRecords is a list of address records found in the response.
|
||||
var addressRecords map[string][]netip.Addr
|
||||
|
||||
for {
|
||||
h, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
@@ -222,83 +232,171 @@ nextAnswer:
|
||||
}
|
||||
continue
|
||||
}
|
||||
if h.Type != dnsmessage.TypeA && h.Type != dnsmessage.TypeAAAA {
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
domain := h.Name.String()
|
||||
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
|
||||
if len(domain) == 0 {
|
||||
return
|
||||
}
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
domain = strings.ToLower(domain)
|
||||
e.logf("[v2] observed DNS response for %s", domain)
|
||||
|
||||
e.mu.Lock()
|
||||
addrs, ok := e.domains[domain]
|
||||
// match wildcard domains
|
||||
if !ok {
|
||||
for _, wc := range e.wildcards {
|
||||
if dnsname.HasSuffix(domain, wc) {
|
||||
e.domains[domain] = nil
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var addr netip.Addr
|
||||
if h.Type == dnsmessage.TypeCNAME {
|
||||
res, err := p.CNAMEResource()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
|
||||
if len(cname) == 0 {
|
||||
continue
|
||||
}
|
||||
mak.Set(&cnameChain, cname, domain)
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeA:
|
||||
r, err := p.AResource()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addr = netip.AddrFrom4(r.A)
|
||||
addr := netip.AddrFrom4(r.A)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
case dnsmessage.TypeAAAA:
|
||||
r, err := p.AAAAResource()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addr = netip.AddrFrom16(r.AAAA)
|
||||
addr := netip.AddrFrom16(r.AAAA)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if slices.Contains(addrs, addr) {
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
for domain, addrs := range addressRecords {
|
||||
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
|
||||
|
||||
// domain and none of the CNAMEs in the chain are routed
|
||||
if !isRouted {
|
||||
continue
|
||||
}
|
||||
for _, route := range e.controlRoutes {
|
||||
if route.Contains(addr) {
|
||||
// record the new address associated with the domain for faster matching in subsequent
|
||||
// requests and for diagnostic records.
|
||||
e.mu.Lock()
|
||||
e.domains[domain] = append(addrs, addr)
|
||||
e.mu.Unlock()
|
||||
continue nextAnswer
|
||||
|
||||
// advertise each address we have learned for the routed domain, that
|
||||
// was not already known.
|
||||
var toAdvertise []netip.Prefix
|
||||
for _, addr := range addrs {
|
||||
if !e.isAddrKnownLocked(domain, addr) {
|
||||
toAdvertise = append(toAdvertise, netip.PrefixFrom(addr, addr.BitLen()))
|
||||
}
|
||||
}
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(netip.PrefixFrom(addr, addr.BitLen())); err != nil {
|
||||
e.logf("failed to advertise route for %s: %v: %v", domain, addr, err)
|
||||
continue
|
||||
}
|
||||
e.logf("[v2] advertised route for %v: %v", domain, addr)
|
||||
|
||||
e.mu.Lock()
|
||||
e.domains[domain] = append(addrs, addr)
|
||||
e.mu.Unlock()
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
}
|
||||
}
|
||||
|
||||
// starting from the given domain that resolved to an address, find it, or any
|
||||
// of the domains in the CNAME chain toward resolving it, that are routed
|
||||
// domains, returning the routed domain name and a bool indicating whether a
|
||||
// routed domain was found.
|
||||
// e.mu must be held.
|
||||
func (e *AppConnector) findRoutedDomainLocked(domain string, cnameChain map[string]string) (string, bool) {
|
||||
var isRouted bool
|
||||
for {
|
||||
_, isRouted = e.domains[domain]
|
||||
if isRouted {
|
||||
break
|
||||
}
|
||||
|
||||
// match wildcard domains
|
||||
for _, wc := range e.wildcards {
|
||||
if dnsname.HasSuffix(domain, wc) {
|
||||
e.domains[domain] = nil
|
||||
isRouted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
next, ok := cnameChain[domain]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
domain = next
|
||||
}
|
||||
return domain, isRouted
|
||||
}
|
||||
|
||||
// isAddrKnownLocked returns true if the address is known to be associated with
|
||||
// the given domain. Known domain tables are updated for covered routes to speed
|
||||
// up future matches.
|
||||
// e.mu must be held.
|
||||
func (e *AppConnector) isAddrKnownLocked(domain string, addr netip.Addr) bool {
|
||||
if e.hasDomainAddrLocked(domain, addr) {
|
||||
return true
|
||||
}
|
||||
for _, route := range e.controlRoutes {
|
||||
if route.Contains(addr) {
|
||||
// record the new address associated with the domain for faster matching in subsequent
|
||||
// requests and for diagnostic records.
|
||||
e.addDomainAddrLocked(domain, addr)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// scheduleAdvertisement schedules an advertisement of the given address
|
||||
// associated with the given domain.
|
||||
func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Prefix) {
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
|
||||
e.logf("failed to advertise routes for %s: %v: %v", domain, routes, err)
|
||||
return
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
for _, route := range routes {
|
||||
if !route.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
addr := route.Addr()
|
||||
if !e.hasDomainAddrLocked(domain, addr) {
|
||||
e.addDomainAddrLocked(domain, addr)
|
||||
e.logf("[v2] advertised route for %v: %v", domain, addr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// hasDomainAddrLocked returns true if the address has been observed in a
|
||||
// resolution of domain.
|
||||
func (e *AppConnector) hasDomainAddrLocked(domain string, addr netip.Addr) bool {
|
||||
_, ok := slices.BinarySearchFunc(e.domains[domain], addr, compareAddr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// addDomainAddrLocked adds the address to the list of addresses resolved for
|
||||
// domain and ensures the list remains sorted. Does not attempt to deduplicate.
|
||||
func (e *AppConnector) addDomainAddrLocked(domain string, addr netip.Addr) {
|
||||
e.domains[domain] = append(e.domains[domain], addr)
|
||||
slices.SortFunc(e.domains[domain], compareAddr)
|
||||
}
|
||||
|
||||
func compareAddr(l, r netip.Addr) int {
|
||||
return l.Compare(r)
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ func TestDomainRoutes(t *testing.T) {
|
||||
a := NewAppConnector(t.Logf, rc)
|
||||
a.updateDomains([]string{"example.com"})
|
||||
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
|
||||
a.Wait(context.Background())
|
||||
|
||||
want := map[string][]netip.Addr{
|
||||
"example.com": {netip.MustParseAddr("192.0.0.8")},
|
||||
@@ -110,6 +111,7 @@ func TestDomainRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestObserveDNSResponse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rc := &appctest.RouteCollector{}
|
||||
a := NewAppConnector(t.Logf, rc)
|
||||
|
||||
@@ -123,6 +125,26 @@ func TestObserveDNSResponse(t *testing.T) {
|
||||
|
||||
a.updateDomains([]string{"example.com"})
|
||||
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
|
||||
a.Wait(ctx)
|
||||
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
|
||||
// a CNAME record chain should result in a route being added if the chain
|
||||
// matches a routed domain.
|
||||
a.updateDomains([]string{"www.example.com", "example.com"})
|
||||
a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.9", "www.example.com.", "chain.example.com.", "example.com."))
|
||||
a.Wait(ctx)
|
||||
wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.9/32"))
|
||||
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
|
||||
// a CNAME record chain should result in a route being added if the chain
|
||||
// even if only found in the middle of the chain
|
||||
a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.10", "outside.example.org.", "www.example.com.", "example.org."))
|
||||
a.Wait(ctx)
|
||||
wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.10/32"))
|
||||
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
@@ -130,12 +152,14 @@ func TestObserveDNSResponse(t *testing.T) {
|
||||
wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
|
||||
|
||||
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
|
||||
a.Wait(ctx)
|
||||
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
|
||||
// don't re-advertise routes that have already been advertised
|
||||
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
|
||||
a.Wait(ctx)
|
||||
if !slices.Equal(rc.Routes(), wantRoutes) {
|
||||
t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
|
||||
}
|
||||
@@ -145,6 +169,7 @@ func TestObserveDNSResponse(t *testing.T) {
|
||||
a.updateRoutes([]netip.Prefix{pfx})
|
||||
wantRoutes = append(wantRoutes, pfx)
|
||||
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.2.1"))
|
||||
a.Wait(ctx)
|
||||
if !slices.Equal(rc.Routes(), wantRoutes) {
|
||||
t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
|
||||
}
|
||||
@@ -154,11 +179,13 @@ func TestObserveDNSResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWildcardDomains(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rc := &appctest.RouteCollector{}
|
||||
a := NewAppConnector(t.Logf, rc)
|
||||
|
||||
a.updateDomains([]string{"*.example.com"})
|
||||
a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8"))
|
||||
a.Wait(ctx)
|
||||
if got, want := rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) {
|
||||
t.Errorf("routes: got %v; want %v", got, want)
|
||||
}
|
||||
@@ -218,6 +245,61 @@ func dnsResponse(domain, address string) []byte {
|
||||
return must.Get(b.Finish())
|
||||
}
|
||||
|
||||
func dnsCNAMEResponse(address string, domains ...string) []byte {
|
||||
addr := netip.MustParseAddr(address)
|
||||
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
|
||||
b.EnableCompression()
|
||||
b.StartAnswers()
|
||||
|
||||
if len(domains) >= 2 {
|
||||
for i, domain := range domains[:len(domains)-1] {
|
||||
b.CNAMEResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName(domain),
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.CNAMEResource{
|
||||
CNAME: dnsmessage.MustNewName(domains[i+1]),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
domain := domains[len(domains)-1]
|
||||
|
||||
switch addr.BitLen() {
|
||||
case 32:
|
||||
b.AResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName(domain),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.AResource{
|
||||
A: addr.As4(),
|
||||
},
|
||||
)
|
||||
case 128:
|
||||
b.AAAAResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName(domain),
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.AAAAResource{
|
||||
AAAA: addr.As16(),
|
||||
},
|
||||
)
|
||||
default:
|
||||
panic("invalid address length")
|
||||
}
|
||||
return must.Get(b.Finish())
|
||||
}
|
||||
|
||||
func prefixEqual(a, b netip.Prefix) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package tailscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
@@ -34,10 +35,10 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
// defaultLocalClient is the default LocalClient when using the legacy
|
||||
@@ -479,7 +480,7 @@ func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts)
|
||||
opts = &DebugPortmapOpts{}
|
||||
}
|
||||
|
||||
vals.Set("duration", cmpx.Or(opts.Duration, 5*time.Second).String())
|
||||
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String())
|
||||
vals.Set("type", opts.Type)
|
||||
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP))
|
||||
|
||||
@@ -1417,6 +1418,48 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
|
||||
return &cv, nil
|
||||
}
|
||||
|
||||
// TailFSSetFileServerAddr instructs TailFS to use the server at addr to access
|
||||
// the filesystem. This is used on platforms like Windows and MacOS to let
|
||||
// TailFS know to use the file server running in the GUI app.
|
||||
func (lc *LocalClient) TailFSSetFileServerAddr(ctx context.Context, addr string) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareAdd adds the given share to the list of shares that TailFS will
|
||||
// serve to remote nodes. If a share with the same name already exists, the
|
||||
// existing share is replaced/updated.
|
||||
func (lc *LocalClient) TailFSShareAdd(ctx context.Context, share *tailfs.Share) error {
|
||||
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareRemove removes the share with the given name from the list of
|
||||
// shares that TailFS will serve to remote nodes.
|
||||
func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error {
|
||||
_, err := lc.send(
|
||||
ctx,
|
||||
"DELETE",
|
||||
"/localapi/v0/tailfs/shares",
|
||||
http.StatusNoContent,
|
||||
jsonBody(&tailfs.Share{
|
||||
Name: name,
|
||||
}))
|
||||
return err
|
||||
}
|
||||
|
||||
// TailFSShareList returns the list of shares that TailFS is currently serving
|
||||
// to remote nodes.
|
||||
func (lc *LocalClient) TailFSShareList(ctx context.Context) (map[string]*tailfs.Share, error) {
|
||||
result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var shares map[string]*tailfs.Share
|
||||
err = json.Unmarshal(result, &shares)
|
||||
return shares, err
|
||||
}
|
||||
|
||||
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
|
||||
// It's returned by LocalClient.WatchIPNBus.
|
||||
//
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
|
||||
package tailscale
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/deptest"
|
||||
)
|
||||
|
||||
func TestGetServeConfigFromJSON(t *testing.T) {
|
||||
sc, err := getServeConfigFromJSON([]byte("null"))
|
||||
@@ -25,3 +29,14 @@ func TestGetServeConfigFromJSON(t *testing.T) {
|
||||
t.Errorf("want non-nil TCP for object")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
// Make sure we don't again accidentally bring in a dependency on
|
||||
// TailFS or its transitive dependencies
|
||||
"tailscale.com/tailfs/tailfsimpl": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
"github.com/tailscale/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -269,11 +269,22 @@ type capRule struct {
|
||||
|
||||
// toPeerCapabilities parses out the web ui capabilities from the
|
||||
// given whois response.
|
||||
func toPeerCapabilities(whois *apitype.WhoIsResponse) (peerCapabilities, error) {
|
||||
caps := peerCapabilities{}
|
||||
func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) (peerCapabilities, error) {
|
||||
if whois == nil {
|
||||
return caps, nil
|
||||
return peerCapabilities{}, nil
|
||||
}
|
||||
|
||||
if !status.Self.IsTagged() {
|
||||
// User owned nodes are only ever manageable by the owner.
|
||||
if status.Self.UserID != whois.UserProfile.ID {
|
||||
return peerCapabilities{}, nil
|
||||
} else {
|
||||
return peerCapabilities{capFeatureAll: true}, nil // owner can edit all features
|
||||
}
|
||||
}
|
||||
|
||||
// For tagged nodes, we actually look at the granted capabilities.
|
||||
caps := peerCapabilities{}
|
||||
rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal capability: %v", err)
|
||||
|
||||
@@ -25,13 +25,14 @@
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-curly-quotes": "^1.0.4",
|
||||
"jsdom": "^23.0.1",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.3.9",
|
||||
"vite": "^4.5.2",
|
||||
"vite-plugin-rewrite-all": "^1.0.1",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
@@ -50,9 +51,11 @@
|
||||
"react-app"
|
||||
],
|
||||
"plugins": [
|
||||
"curly-quotes",
|
||||
"react-hooks"
|
||||
],
|
||||
"rules": {
|
||||
"curly-quotes/no-straight-quotes": "warn",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error"
|
||||
},
|
||||
|
||||
@@ -180,7 +180,7 @@ export default function ExitNodeSelector({
|
||||
)}
|
||||
{pending && (
|
||||
<p className="text-white p-3">
|
||||
Pending approval to run as exit node. This device won't be usable as
|
||||
Pending approval to run as exit node. This device won’t be usable as
|
||||
an exit node until then.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -162,7 +162,19 @@ function LoginPopoverContent({
|
||||
</div>
|
||||
{!auth.canManageNode && (
|
||||
<>
|
||||
{!auth.viewerIdentity ? (
|
||||
{auth.serverMode === "readonly" ? (
|
||||
<p className="text-gray-500 text-xs">
|
||||
This web interface is running in read-only mode.{" "}
|
||||
<a
|
||||
href="https://tailscale.com/s/web-client-read-only"
|
||||
className="text-blue-700"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more →
|
||||
</a>
|
||||
</p>
|
||||
) : !auth.viewerIdentity ? (
|
||||
// User is not connected over Tailscale.
|
||||
// These states are only possible on the login client.
|
||||
<>
|
||||
@@ -178,7 +190,7 @@ function LoginPopoverContent({
|
||||
) : (
|
||||
// ACLs allow access, but user can't connect.
|
||||
<>
|
||||
Cannot access this device's Tailscale IP. Make sure you
|
||||
Cannot access this device’s Tailscale IP. Make sure you
|
||||
are connected to your tailnet, and that your policy file
|
||||
allows access.
|
||||
</>
|
||||
@@ -197,7 +209,7 @@ function LoginPopoverContent({
|
||||
// User can connect to Tailcale IP; sign in when ready.
|
||||
<>
|
||||
<p className="text-gray-500 text-xs">
|
||||
You can see most of this device's details. To make changes,
|
||||
You can see most of this device’s details. To make changes,
|
||||
you need to sign in.
|
||||
</p>
|
||||
{isHTTPS && (
|
||||
|
||||
@@ -61,7 +61,7 @@ export function ChangelogText({ version }: { version?: string }) {
|
||||
<a href="https://tailscale.com/changelog/" className="link">
|
||||
release notes
|
||||
</a>{" "}
|
||||
to find out what's new!
|
||||
to find out what’s new!
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function LoginView({ data }: { data: NodeData }) {
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700">
|
||||
Your device's key has expired. Reauthenticate this device by
|
||||
Your device’s key has expired. Reauthenticate this device by
|
||||
logging in again, or{" "}
|
||||
<a
|
||||
href="https://tailscale.com/kb/1028/key-expiry"
|
||||
|
||||
@@ -35,7 +35,7 @@ export function UpdatingView({
|
||||
<Spinner size="sm" className="text-gray-400" />
|
||||
<h1 className="text-2xl m-3">Update in progress</h1>
|
||||
<p className="text-gray-400">
|
||||
The update shouldn't take more than a couple of minutes. Once it's
|
||||
The update shouldn’t take more than a couple of minutes. Once it’s
|
||||
completed, you will be asked to log in again.
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ export enum AuthType {
|
||||
export type AuthResponse = {
|
||||
authNeeded?: AuthType
|
||||
canManageNode: boolean
|
||||
serverMode: "login" | "manage"
|
||||
serverMode: "login" | "readonly" | "manage"
|
||||
viewerIdentity?: {
|
||||
loginName: string
|
||||
nodeName: string
|
||||
|
||||
@@ -95,6 +95,14 @@ const (
|
||||
// In this mode, API calls are authenticated via platform auth.
|
||||
LoginServerMode ServerMode = "login"
|
||||
|
||||
// ReadOnlyServerMode is identical to LoginServerMode,
|
||||
// but does not present a login button to switch to manage mode,
|
||||
// even if the management client is running and reachable.
|
||||
//
|
||||
// This is designed for platforms where the device is configured by other means,
|
||||
// such as Home Assistant's declarative YAML configuration.
|
||||
ReadOnlyServerMode ServerMode = "readonly"
|
||||
|
||||
// ManageServerMode serves a management client for editing tailscale
|
||||
// settings of a node.
|
||||
//
|
||||
@@ -154,7 +162,7 @@ type ServerOpts struct {
|
||||
// and not the lifespan of the web server.
|
||||
func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
switch opts.Mode {
|
||||
case LoginServerMode, ManageServerMode:
|
||||
case LoginServerMode, ReadOnlyServerMode, ManageServerMode:
|
||||
// valid types
|
||||
case "":
|
||||
return nil, fmt.Errorf("must specify a Mode")
|
||||
@@ -207,10 +215,14 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
// The client is secured by limiting the interface it listens on,
|
||||
// or by authenticating requests before they reach the web client.
|
||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
||||
if s.mode == LoginServerMode {
|
||||
switch s.mode {
|
||||
case LoginServerMode:
|
||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
|
||||
metric = "web_login_client_initialization"
|
||||
} else {
|
||||
case ReadOnlyServerMode:
|
||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
|
||||
metric = "web_readonly_client_initialization"
|
||||
case ManageServerMode:
|
||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
|
||||
metric = "web_client_initialization"
|
||||
}
|
||||
@@ -465,7 +477,7 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
|
||||
session, whois, status, sErr := s.getSession(r)
|
||||
|
||||
if whois != nil {
|
||||
caps, err := toPeerCapabilities(whois)
|
||||
caps, err := toPeerCapabilities(status, whois)
|
||||
if err != nil {
|
||||
http.Error(w, sErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -483,7 +495,7 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// First verify platform auth.
|
||||
// If platform auth is needed, this should happen first.
|
||||
if s.mode == LoginServerMode {
|
||||
if s.mode == LoginServerMode || s.mode == ReadOnlyServerMode {
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
authorized, err := authorizeSynology(r)
|
||||
|
||||
@@ -450,7 +450,7 @@ func TestServeAuth(t *testing.T) {
|
||||
NodeName: remoteNode.Node.Name,
|
||||
NodeIP: remoteIP,
|
||||
ProfilePicURL: user.ProfilePicURL,
|
||||
Capabilities: peerCapabilities{},
|
||||
Capabilities: peerCapabilities{capFeatureAll: true},
|
||||
}
|
||||
|
||||
testControlURL := &defaultControlURL
|
||||
@@ -1099,19 +1099,52 @@ func TestRequireTailscaleIP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPeerCapabilities(t *testing.T) {
|
||||
userOwnedStatus := &ipnstate.Status{Self: &ipnstate.PeerStatus{UserID: tailcfg.UserID(1)}}
|
||||
tags := views.SliceOf[string]([]string{"tag:server"})
|
||||
tagOwnedStatus := &ipnstate.Status{Self: &ipnstate.PeerStatus{Tags: &tags}}
|
||||
|
||||
// Testing web.toPeerCapabilities
|
||||
toPeerCapsTests := []struct {
|
||||
name string
|
||||
status *ipnstate.Status
|
||||
whois *apitype.WhoIsResponse
|
||||
wantCaps peerCapabilities
|
||||
}{
|
||||
{
|
||||
name: "empty-whois",
|
||||
status: userOwnedStatus,
|
||||
whois: nil,
|
||||
wantCaps: peerCapabilities{},
|
||||
},
|
||||
{
|
||||
name: "no-webui-caps",
|
||||
name: "user-owned-node-non-owner-caps-ignored",
|
||||
status: userOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(2)},
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaps: peerCapabilities{},
|
||||
},
|
||||
{
|
||||
name: "user-owned-node-owner-caps-ignored",
|
||||
status: userOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(1)},
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaps: peerCapabilities{capFeatureAll: true}, // should just have wildcard
|
||||
},
|
||||
{
|
||||
name: "tag-owned-no-webui-caps",
|
||||
status: tagOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityDebugPeer: []tailcfg.RawMessage{},
|
||||
@@ -1120,7 +1153,8 @@ func TestPeerCapabilities(t *testing.T) {
|
||||
wantCaps: peerCapabilities{},
|
||||
},
|
||||
{
|
||||
name: "one-webui-cap",
|
||||
name: "tag-owned-one-webui-cap",
|
||||
status: tagOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
@@ -1134,7 +1168,8 @@ func TestPeerCapabilities(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-webui-cap",
|
||||
name: "tag-owned-multiple-webui-cap",
|
||||
status: tagOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
@@ -1151,7 +1186,8 @@ func TestPeerCapabilities(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "case=insensitive-caps",
|
||||
name: "tag-owned-case-insensitive-caps",
|
||||
status: tagOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
@@ -1165,7 +1201,8 @@ func TestPeerCapabilities(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "random-canEdit-contents-dont-error",
|
||||
name: "tag-owned-random-canEdit-contents-dont-error",
|
||||
status: tagOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
@@ -1178,7 +1215,8 @@ func TestPeerCapabilities(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-canEdit-section",
|
||||
name: "tag-owned-no-canEdit-section",
|
||||
status: tagOwnedStatus,
|
||||
whois: &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{
|
||||
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
|
||||
@@ -1191,7 +1229,7 @@ func TestPeerCapabilities(t *testing.T) {
|
||||
}
|
||||
for _, tt := range toPeerCapsTests {
|
||||
t.Run("toPeerCapabilities-"+tt.name, func(t *testing.T) {
|
||||
got, err := toPeerCapabilities(tt.whois)
|
||||
got, err := toPeerCapabilities(tt.status, tt.whois)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %v", err)
|
||||
}
|
||||
|
||||
@@ -3049,6 +3049,11 @@ eslint-module-utils@^2.8.0:
|
||||
dependencies:
|
||||
debug "^3.2.7"
|
||||
|
||||
eslint-plugin-curly-quotes@^1.0.4:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-curly-quotes/-/eslint-plugin-curly-quotes-1.0.10.tgz#dd61a1d6bb48123d842e21f2086f146c6ab5b8b0"
|
||||
integrity sha512-v2SryrqXE8EEMgc7VSOpyQVZP1dy3N4aS5ZpTor2aV7/ltXlcX6kxpb/Ls5MoHz8ICheVoTuTwqYnSKFgjQUow==
|
||||
|
||||
eslint-plugin-flowtype@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912"
|
||||
@@ -5241,10 +5246,10 @@ vite-tsconfig-paths@^3.5.0:
|
||||
recrawl-sync "^2.0.3"
|
||||
tsconfig-paths "^4.0.0"
|
||||
|
||||
"vite@^3.0.0 || ^4.0.0", vite@^4.3.9:
|
||||
version "4.4.9"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
|
||||
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
|
||||
"vite@^3.0.0 || ^4.0.0", vite@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
|
||||
integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
|
||||
dependencies:
|
||||
esbuild "^0.18.10"
|
||||
postcss "^8.4.27"
|
||||
|
||||
@@ -167,6 +167,10 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
||||
return up.updateWindows, true
|
||||
case "linux":
|
||||
switch distro.Get() {
|
||||
case distro.NixOS:
|
||||
// NixOS packages are immutable and managed with a system-wide
|
||||
// configuration.
|
||||
return up.updateNixos, false
|
||||
case distro.Synology:
|
||||
// Synology updates use our own pkgs.tailscale.com instead of the
|
||||
// Synology Package Center. We should eventually get to a regular
|
||||
@@ -522,6 +526,13 @@ func (up *Updater) updateArchLike() error {
|
||||
you can use "pacman --sync --refresh --sysupgrade" or "pacman -Syu" to upgrade the system, including Tailscale.`)
|
||||
}
|
||||
|
||||
func (up *Updater) updateNixos() error {
|
||||
// NixOS package updates are managed on a system level and not individually.
|
||||
// Direct users to update their nix channel or nixpkgs flake input to
|
||||
// receive the latest version.
|
||||
return errors.New(`individual package updates are not supported on NixOS installations. Update your system channel or flake inputs to get the latest Tailscale version from nixpkgs.`)
|
||||
}
|
||||
|
||||
const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
|
||||
|
||||
// updateFedoraLike updates tailscale on any distros in the Fedora family,
|
||||
|
||||
@@ -55,6 +55,21 @@
|
||||
// and not `tailscale up` or `tailscale set`.
|
||||
// The config file contents are currently read once on container start.
|
||||
// NB: This env var is currently experimental and the logic will likely change!
|
||||
// - EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS: if set to true
|
||||
// and if this containerboot instance is an L7 ingress proxy (created by
|
||||
// the Kubernetes operator), set up rules to allow proxying cluster traffic,
|
||||
// received on the Pod IP of this node, to the ingress target in the cluster.
|
||||
// This, in conjunction with MagicDNS name resolution in cluster, can be
|
||||
// useful for cases where a cluster workload needs to access a target in
|
||||
// cluster using the same hostname (in this case, the MagicDNS name of the ingress proxy)
|
||||
// as a non-cluster workload on tailnet.
|
||||
// This is only meant to be configured by the Kubernetes operator.
|
||||
// - EXPERIMENTAL_AUTH_KEYS_ENDPOINT: if set and if running in Kubernetes, auth
|
||||
// key will be retrieved by POST request to the endpoint passing service
|
||||
// account token as an auth token. This is used by the Tailscale Kubernetes
|
||||
// operator who also runs the endpoint.
|
||||
// Tailscale IP range to DNAT to.
|
||||
// - EXPERIMENTAL_TS_VIP // i.e 1.2.3.4
|
||||
//
|
||||
// When running on Kubernetes, containerboot defaults to storing state in the
|
||||
// "tailscale" kube secret. To store state on local disk instead, set
|
||||
@@ -71,8 +86,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -108,29 +125,35 @@ func newNetfilterRunner(logf logger.Logf) (linuxfw.NetfilterRunner, error) {
|
||||
func main() {
|
||||
log.SetPrefix("boot: ")
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
cfg := &settings{
|
||||
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
|
||||
Hostname: defaultEnv("TS_HOSTNAME", ""),
|
||||
Routes: defaultEnvStringPointer("TS_ROUTES"),
|
||||
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
|
||||
ProxyTo: defaultEnv("TS_DEST_IP", ""),
|
||||
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
|
||||
TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
|
||||
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
|
||||
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
|
||||
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
|
||||
UserspaceMode: defaultBool("TS_USERSPACE", true),
|
||||
StateDir: defaultEnv("TS_STATE_DIR", ""),
|
||||
AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
|
||||
KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
|
||||
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
|
||||
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
|
||||
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
|
||||
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
|
||||
TailscaledConfigFilePath: defaultEnv("EXPERIMENTAL_TS_CONFIGFILE_PATH", ""),
|
||||
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
|
||||
Hostname: defaultEnv("TS_HOSTNAME", ""),
|
||||
Routes: defaultEnvStringPointer("TS_ROUTES"),
|
||||
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
|
||||
ProxyTo: defaultEnv("TS_DEST_IP", ""),
|
||||
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
|
||||
TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
|
||||
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
|
||||
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
|
||||
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
|
||||
UserspaceMode: defaultBool("TS_USERSPACE", true),
|
||||
StateDir: defaultEnv("TS_STATE_DIR", ""),
|
||||
AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
|
||||
KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
|
||||
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
|
||||
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
|
||||
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
|
||||
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
|
||||
TailscaledConfigFilePath: defaultEnv("EXPERIMENTAL_TS_CONFIGFILE_PATH", ""),
|
||||
AllowProxyingClusterTrafficViaIngress: defaultBool("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS", false),
|
||||
PodIP: defaultEnv("POD_IP", ""),
|
||||
PodName: defaultEnv("POD_NAME", ""),
|
||||
KeysEndpoint: defaultEnv("EXPERIMENTAL_AUTH_KEYS_ENDPOINT", ""),
|
||||
KubeStateSecret: defaultEnv("EXPERIMENTAL_KUBE_STATE_SECRET", ""),
|
||||
TSVIP: defaultEnv("EXPERIMENTAL_TS_VIP", ""),
|
||||
}
|
||||
|
||||
if err := cfg.validate(); err != nil {
|
||||
log.Fatalf("invalid configuration: %v", err)
|
||||
}
|
||||
@@ -169,7 +192,10 @@ func main() {
|
||||
}
|
||||
cfg.KubernetesCanPatch = canPatch
|
||||
|
||||
if cfg.AuthKey == "" && !isOneStepConfig(cfg) {
|
||||
// TODO: check that can do token request maybe?
|
||||
|
||||
// TODO: did I break something here?
|
||||
if authKeySourceIsKubeSecret(cfg) {
|
||||
key, err := findKeyInKubeSecret(bootCtx, cfg.KubeSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("Getting authkey from kube secret: %v", err)
|
||||
@@ -241,6 +267,18 @@ func main() {
|
||||
}
|
||||
didLogin = true
|
||||
w.Close()
|
||||
if cfg.KeysEndpoint != "" {
|
||||
log.Printf("Creating Tailscale authkey by calling %s", cfg.KeysEndpoint)
|
||||
key, err := getAuthKey(context.Background(), cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("error getting Tailscale auth key: %v", err)
|
||||
}
|
||||
// TODO: this will not work with declarative config file
|
||||
// that wants the auth key in there- figure out how to
|
||||
// fix
|
||||
// (So for now this does not work with Connector proxies)
|
||||
cfg.AuthKey = string(key)
|
||||
}
|
||||
if err := tailscaleUp(bootCtx, cfg); err != nil {
|
||||
return fmt.Errorf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
@@ -251,6 +289,8 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Never with the Tailscale Kubernetes operator as it always sets
|
||||
// cfg.AuthOnce=true
|
||||
if isTwoStepConfigAlwaysAuth(cfg) {
|
||||
if err := authTailscale(); err != nil {
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
@@ -268,13 +308,17 @@ authLoop:
|
||||
switch *n.State {
|
||||
case ipn.NeedsLogin:
|
||||
if isOneStepConfig(cfg) {
|
||||
// This could happen if this is the
|
||||
// first time tailscaled was run for
|
||||
// this device and the auth key was not
|
||||
// passed via the configfile.
|
||||
// if state secret is set, delete it to
|
||||
// ensure that we start from a clean
|
||||
// slate on next restart. This could
|
||||
// happen if this is the first time
|
||||
// tailscaled was run for this device
|
||||
// and the auth key was not passed via
|
||||
// the configfile.
|
||||
log.Fatalf("invalid state: tailscaled daemon started with a config file, but tailscale is not logged in: ensure you pass a valid auth key in the config file.")
|
||||
}
|
||||
if err := authTailscale(); err != nil {
|
||||
// delete state secret if set
|
||||
log.Fatalf("failed to auth tailscale: %v", err)
|
||||
}
|
||||
case ipn.NeedsMachineAuth:
|
||||
@@ -330,7 +374,7 @@ authLoop:
|
||||
}
|
||||
|
||||
var (
|
||||
wantProxy = cfg.ProxyTo != "" || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != ""
|
||||
wantProxy = cfg.ProxyTo != "" || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != "" || cfg.AllowProxyingClusterTrafficViaIngress
|
||||
wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch
|
||||
startupTasksDone = false
|
||||
currentIPs deephash.Sum // tailscale IPs assigned to device
|
||||
@@ -365,6 +409,19 @@ authLoop:
|
||||
}
|
||||
}()
|
||||
var wg sync.WaitGroup
|
||||
// We only need to do this once. Backend target change or VIP change
|
||||
// comes in via env var change which would trigger restart.
|
||||
if cfg.ProxyTo != "" && cfg.TSVIP != "" {
|
||||
netIP, err := netip.ParsePrefix(cfg.TSVIP)
|
||||
if err != nil {
|
||||
log.Fatalf("error parsing VIP %s: %v", cfg.TSVIP, err)
|
||||
}
|
||||
log.Printf("Installing proxy rules for a virtual tailnet IP: %s", cfg.TSVIP)
|
||||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, []netip.Prefix{netIP}, nfr); err != nil {
|
||||
log.Fatalf("installing ingress proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
runLoop:
|
||||
for {
|
||||
select {
|
||||
@@ -429,7 +486,7 @@ runLoop:
|
||||
}
|
||||
currentEgressIPs = newCurentEgressIPs
|
||||
}
|
||||
if cfg.ProxyTo != "" && len(addrs) > 0 && ipsHaveChanged {
|
||||
if cfg.ProxyTo != "" && cfg.TSVIP == "" && len(addrs) > 0 && ipsHaveChanged {
|
||||
log.Printf("Installing proxy rules")
|
||||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs, nfr); err != nil {
|
||||
log.Fatalf("installing ingress proxy rules: %v", err)
|
||||
@@ -451,6 +508,18 @@ runLoop:
|
||||
log.Fatalf("installing egress proxy rules: %v", err)
|
||||
}
|
||||
}
|
||||
// If this is a L7 cluster ingress proxy (set up
|
||||
// by Kubernetes operator) and proxying of
|
||||
// cluster traffic to the ingress target is
|
||||
// enabled, set up proxy rule each time the
|
||||
// tailnet IPs of this node change (including
|
||||
// the first time they become available).
|
||||
if cfg.AllowProxyingClusterTrafficViaIngress && cfg.ServeConfigPath != "" && ipsHaveChanged && len(addrs) > 0 {
|
||||
log.Printf("installing rules to forward traffic for %s to node's tailnet IP", cfg.PodIP)
|
||||
if err := installTSForwardingRuleForDestination(ctx, cfg.PodIP, addrs, nfr); err != nil {
|
||||
log.Fatalf("installing rules to forward traffic to node's tailnet IP: %v", err)
|
||||
}
|
||||
}
|
||||
currentIPs = newCurrentIPs
|
||||
|
||||
deviceInfo := []any{n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name()}
|
||||
@@ -615,10 +684,18 @@ func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient
|
||||
func tailscaledArgs(cfg *settings) []string {
|
||||
args := []string{"--socket=" + cfg.Socket}
|
||||
switch {
|
||||
case cfg.InKubernetes && cfg.KubeSecret != "":
|
||||
args = append(args, "--state=kube:"+cfg.KubeSecret)
|
||||
if cfg.StateDir == "" {
|
||||
cfg.StateDir = "/tmp"
|
||||
case cfg.InKubernetes:
|
||||
if cfg.KeysEndpoint != "" {
|
||||
stateSecretName := fmt.Sprintf("ts-state-%s", cfg.PodName)
|
||||
args = append(args, "--state=kube:"+stateSecretName)
|
||||
if cfg.StateDir == "" {
|
||||
cfg.StateDir = "/tmp"
|
||||
}
|
||||
} else if cfg.KubeSecret != "" {
|
||||
args = append(args, "--state=kube:"+cfg.KubeSecret)
|
||||
if cfg.StateDir == "" {
|
||||
cfg.StateDir = "/tmp"
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case cfg.StateDir != "":
|
||||
@@ -837,11 +914,43 @@ func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []net
|
||||
return nil
|
||||
}
|
||||
|
||||
// installTSForwardingRuleForDestination accepts a destination address and a
|
||||
// list of node's tailnet addresses, sets up rules to forward traffic for
|
||||
// destination to the tailnet IP matching the destination IP family.
|
||||
// Destination can be Pod IP of this node.
|
||||
func installTSForwardingRuleForDestination(ctx context.Context, dstFilter string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
dst, err := netip.ParseAddr(dstFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var local netip.Addr
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr()
|
||||
break
|
||||
}
|
||||
if !local.IsValid() {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstFilter, tsIPs)
|
||||
}
|
||||
if err := nfr.AddDNATRule(dst, local); err != nil {
|
||||
return fmt.Errorf("installing rule for forwarding traffic to tailnet IP: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// local can be either the Tailnet IP address of this Tailscale device
|
||||
// or it can be a tailnet virtual IP that this tailnet node is a backend
|
||||
// for.
|
||||
var local netip.Addr
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
@@ -890,6 +999,7 @@ type settings struct {
|
||||
StateDir string
|
||||
AcceptDNS *bool
|
||||
KubeSecret string
|
||||
KubeStateSecret string
|
||||
SOCKSProxyAddr string
|
||||
HTTPProxyAddr string
|
||||
Socket string
|
||||
@@ -897,6 +1007,17 @@ type settings struct {
|
||||
Root string
|
||||
KubernetesCanPatch bool
|
||||
TailscaledConfigFilePath string
|
||||
// If set to true and, if this containerboot instance is a Kubernetes
|
||||
// ingress proxy, set up rules to forward incoming cluster traffic to be
|
||||
// forwarded to the ingress target in cluster.
|
||||
AllowProxyingClusterTrafficViaIngress bool
|
||||
// PodIP is the IP of the Pod if running in Kubernetes. This is used
|
||||
// when setting up rules to proxy cluster traffic to cluster ingress
|
||||
// target.
|
||||
PodIP string
|
||||
PodName string
|
||||
KeysEndpoint string
|
||||
TSVIP string
|
||||
}
|
||||
|
||||
func (s *settings) validate() error {
|
||||
@@ -920,6 +1041,18 @@ func (s *settings) validate() error {
|
||||
if s.TailscaledConfigFilePath != "" && (s.AcceptDNS != nil || s.AuthKey != "" || s.Routes != nil || s.ExtraArgs != "" || s.Hostname != "") {
|
||||
return errors.New("EXPERIMENTAL_TS_CONFIGFILE_PATH cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS.")
|
||||
}
|
||||
if s.AllowProxyingClusterTrafficViaIngress && s.UserspaceMode {
|
||||
return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is not supported in userspace mode")
|
||||
}
|
||||
if s.AllowProxyingClusterTrafficViaIngress && s.ServeConfigPath == "" {
|
||||
return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is set but this is not a cluster ingress proxy")
|
||||
}
|
||||
if s.AllowProxyingClusterTrafficViaIngress && s.PodIP == "" {
|
||||
return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is set but POD_IP is not set")
|
||||
}
|
||||
if s.KeysEndpoint != "" && !s.InKubernetes {
|
||||
return errors.New("EXPERIMENTAL_AUTH_KEYS_ENDPOINT is set, but the containerboot does not appear to be running on kube")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1019,3 +1152,37 @@ func isTwoStepConfigAlwaysAuth(cfg *settings) bool {
|
||||
func isOneStepConfig(cfg *settings) bool {
|
||||
return cfg.TailscaledConfigFilePath != ""
|
||||
}
|
||||
|
||||
func authKeySourceIsKubeSecret(cfg *settings) bool {
|
||||
return cfg.InKubernetes && cfg.AuthKey == "" && cfg.KeysEndpoint == ""
|
||||
}
|
||||
|
||||
func getAuthKey(ctx context.Context, cfg *settings) ([]byte, error) {
|
||||
client := http.Client{}
|
||||
// TODO: somewhere check that this has permissions to create a token
|
||||
token, err := kc.CreateTokenForPod(ctx, cfg.PodName, []string{"ts-keyserver"})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating token: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", cfg.KeysEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating new HTTP request: %v", err)
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error requesting auth key from URL %s: %w", cfg.KeysEndpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBs, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
if resp.StatusCode != 200 { // 200 is the only success code returned by the keyserver
|
||||
return nil, fmt.Errorf("auth key response %s with unexpected status code %d", string(respBs), resp.StatusCode)
|
||||
}
|
||||
if len(respBs) == 0 {
|
||||
return nil, errors.New("unexpected empty response")
|
||||
}
|
||||
return respBs, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
@@ -46,7 +46,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter+
|
||||
go4.org/netipx from tailscale.com/net/tsaddr+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
|
||||
google.golang.org/protobuf/encoding/prototext from github.com/prometheus/common/expfmt+
|
||||
@@ -86,7 +86,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/derp from tailscale.com/cmd/derper+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/derper
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/envknob from tailscale.com/derp+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale
|
||||
@@ -94,10 +94,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/metrics from tailscale.com/cmd/derper+
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/net/netmon+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netmon from tailscale.com/net/sockstats+
|
||||
tailscale.com/net/netmon from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
@@ -113,18 +113,19 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs from tailscale.com/client/tailscale
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tstime/rate from tailscale.com/derp+
|
||||
tailscale.com/tsweb from tailscale.com/cmd/derper
|
||||
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
|
||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/lazy from tailscale.com/version+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
@@ -133,34 +134,33 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/types/preftype from tailscale.com/ipn
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnstate+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy+
|
||||
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/views from tailscale.com/ipn+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netmon+
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
tailscale.com/util/cmpx from tailscale.com/cmd/derper+
|
||||
tailscale.com/util/ctxkey from tailscale.com/tsweb+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/mak from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/multierr from tailscale.com/health+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/set from tailscale.com/derp+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
|
||||
tailscale.com/util/syspolicy from tailscale.com/ipn
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb+
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
tailscale.com/version/distro from tailscale.com/envknob+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert
|
||||
golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
|
||||
golang.org/x/crypto/blake2s from tailscale.com/tka
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
@@ -180,10 +180,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/sys/cpu from github.com/josharian/native+
|
||||
LD golang.org/x/sys/unix from github.com/google/nftables+
|
||||
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/registry from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
@@ -195,10 +195,10 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
bytes from bufio+
|
||||
cmp from slices+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
compress/gzip from google.golang.org/protobuf/internal/impl+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto from crypto/ecdh+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
@@ -223,14 +223,14 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from tailscale.com/tka+
|
||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/cmd/derper+
|
||||
expvar from github.com/prometheus/client_golang/prometheus+
|
||||
flag from tailscale.com/cmd/derper+
|
||||
fmt from compress/flate+
|
||||
go/token from google.golang.org/protobuf/internal/strs
|
||||
@@ -244,12 +244,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
io/ioutil from github.com/mitchellh/go-ps+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
maps from tailscale.com/types/views+
|
||||
maps from tailscale.com/ipn+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from mime/multipart+
|
||||
mime from github.com/prometheus/common/expfmt+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
@@ -261,15 +261,15 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/derper
|
||||
W os/user from tailscale.com/util/winutil
|
||||
path from golang.org/x/crypto/acme/autocert+
|
||||
path from github.com/prometheus/client_golang/prometheus/internal+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from internal/profile+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/crypto/acme+
|
||||
runtime/debug from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/metrics from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/pprof from net/http/pprof
|
||||
runtime/trace from net/http/pprof+
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
@@ -34,7 +35,6 @@ import (
|
||||
"tailscale.com/net/stunserver"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -369,7 +369,7 @@ func defaultMeshPSKFile() string {
|
||||
}
|
||||
|
||||
func rateLimitedListenAndServeTLS(srv *http.Server) error {
|
||||
ln, err := net.Listen("tcp", cmpx.Or(srv.Addr, ":https"))
|
||||
ln, err := net.Listen("tcp", cmp.Or(srv.Addr, ":https"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
3
cmd/dist/dist.go
vendored
3
cmd/dist/dist.go
vendored
@@ -33,6 +33,9 @@ func getTargets() ([]dist.Target, error) {
|
||||
// Since only we can provide packages to Synology for
|
||||
// distribution, we default to building the "sideload" variant of
|
||||
// packages that we distribute on pkgs.tailscale.com.
|
||||
//
|
||||
// To build for package center, run
|
||||
// ./tool/go run ./cmd/dist build --synology-package-center synology
|
||||
ret = append(ret, synology.Targets(synologyPackageCenter, nil)...)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -40,7 +40,7 @@ func main() {
|
||||
log.Fatal("at least one tag must be specified")
|
||||
}
|
||||
|
||||
baseURL := cmpx.Or(os.Getenv("TS_BASE_URL"), "https://api.tailscale.com")
|
||||
baseURL := cmp.Or(os.Getenv("TS_BASE_URL"), "https://api.tailscale.com")
|
||||
|
||||
credentials := clientcredentials.Config{
|
||||
ClientID: clientID,
|
||||
|
||||
@@ -162,7 +162,9 @@ func main() {
|
||||
log.Fatal("set either the envvar TS_API_KEY or TS_OAUTH_ID and TS_OAUTH_SECRET")
|
||||
}
|
||||
var client *http.Client
|
||||
if oiok {
|
||||
if oiok && (oauthId != "" || oauthSecret != "") {
|
||||
// Both should ideally be set, but if either are non-empty it means the user had an intent
|
||||
// to set _something_, so they should receive the oauth error flow.
|
||||
oauthConfig := &clientcredentials.Config{
|
||||
ClientID: oauthId,
|
||||
ClientSecret: oauthSecret,
|
||||
|
||||
@@ -430,6 +430,8 @@
|
||||
<footer class="footer text-gray-600 text-center mb-12">
|
||||
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
|
||||
target="_blank">what you can do next →</a></p>
|
||||
<p>Read about <a href="https://tailscale.com/kb/1073/hello" class="text-blue-600 hover:text-blue-800"
|
||||
target="_blank">the Hello service →</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -164,6 +164,17 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
|
||||
hostname = string(cn.Spec.Hostname)
|
||||
}
|
||||
crl := childResourceLabels(cn.Name, a.tsnamespace, "connector")
|
||||
|
||||
proxyClass := cn.Spec.ProxyClass
|
||||
if proxyClass != "" {
|
||||
if ready, err := proxyClassIsReady(ctx, proxyClass, a.Client); err != nil {
|
||||
return fmt.Errorf("error verifying ProxyClass for Connector: %w", err)
|
||||
} else if !ready {
|
||||
logger.Infof("ProxyClass %s specified for the Connector, but is not (yet) Ready, waiting..", proxyClass)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
sts := &tailscaleSTSConfig{
|
||||
ParentResourceName: cn.Name,
|
||||
ParentResourceUID: string(cn.UID),
|
||||
@@ -173,6 +184,7 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
|
||||
Connector: &connector{
|
||||
isExitNode: cn.Spec.ExitNode,
|
||||
},
|
||||
ProxyClass: proxyClass,
|
||||
}
|
||||
|
||||
if cn.Spec.SubnetRouter != nil && len(cn.Spec.SubnetRouter.AdvertiseRoutes) > 0 {
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestConnector(t *testing.T) {
|
||||
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
|
||||
}
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Add another route to be advertised.
|
||||
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
@@ -87,7 +87,7 @@ func TestConnector(t *testing.T) {
|
||||
opts.confFileHash = "fb6c4daf67425f983985750cd8d6f2beae77e614fcb34176604571f5623d6862"
|
||||
expectReconciled(t, cr, "", "test")
|
||||
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Remove a route.
|
||||
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
@@ -96,7 +96,7 @@ func TestConnector(t *testing.T) {
|
||||
opts.subnetRoutes = "10.44.0.0/20"
|
||||
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Remove the subnet router.
|
||||
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
@@ -105,7 +105,7 @@ func TestConnector(t *testing.T) {
|
||||
opts.subnetRoutes = ""
|
||||
opts.confFileHash = "7c421a99128eb80e79a285a82702f19f8f720615542a15bd794858a6275d8079"
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Re-add the subnet router.
|
||||
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
@@ -116,7 +116,7 @@ func TestConnector(t *testing.T) {
|
||||
opts.subnetRoutes = "10.44.0.0/20"
|
||||
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Delete the Connector.
|
||||
if err = fc.Delete(context.Background(), cn); err != nil {
|
||||
@@ -161,7 +161,7 @@ func TestConnector(t *testing.T) {
|
||||
confFileHash: "57d922331890c9b1c8c6ae664394cb254334c551d9cd9db14537b5d9da9fb17e",
|
||||
}
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Add an exit node.
|
||||
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
@@ -170,7 +170,7 @@ func TestConnector(t *testing.T) {
|
||||
opts.isExitNode = true
|
||||
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Delete the Connector.
|
||||
if err = fc.Delete(context.Background(), cn); err != nil {
|
||||
@@ -183,3 +183,109 @@ func TestConnector(t *testing.T) {
|
||||
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
|
||||
}
|
||||
|
||||
func TestConnectorWithProxyClass(t *testing.T) {
|
||||
// Setup
|
||||
pc := &tsapi.ProxyClass{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "custom-metadata"},
|
||||
Spec: tsapi.ProxyClassSpec{StatefulSet: &tsapi.StatefulSet{
|
||||
Labels: map[string]string{"foo": "bar"},
|
||||
Annotations: map[string]string{"bar.io/foo": "some-val"},
|
||||
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
|
||||
}
|
||||
cn := &tsapi.Connector{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: tsapi.ConnectorKind,
|
||||
APIVersion: "tailscale.io/v1alpha1",
|
||||
},
|
||||
Spec: tsapi.ConnectorSpec{
|
||||
SubnetRouter: &tsapi.SubnetRouter{
|
||||
AdvertiseRoutes: []tsapi.Route{"10.40.0.0/14"},
|
||||
},
|
||||
ExitNode: true,
|
||||
},
|
||||
}
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(pc, cn).
|
||||
WithStatusSubresource(pc, cn).
|
||||
Build()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||
cr := &ConnectorReconciler{
|
||||
Client: fc,
|
||||
clock: cl,
|
||||
ssr: &tailscaleSTSReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
},
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// 1. Connector is created with no ProxyClass specified, create
|
||||
// resources with the default configuration.
|
||||
expectReconciled(t, cr, "", "test")
|
||||
fullName, shortName := findGenName(t, fc, "", "test", "connector")
|
||||
|
||||
opts := configOpts{
|
||||
stsName: shortName,
|
||||
secretName: fullName,
|
||||
parentType: "connector",
|
||||
hostname: "test-connector",
|
||||
shouldUseDeclarativeConfig: true,
|
||||
isExitNode: true,
|
||||
subnetRoutes: "10.40.0.0/14",
|
||||
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
|
||||
}
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 2. Update Connector to specify a ProxyClass. ProxyClass is not yet
|
||||
// ready, so its configuration is NOT applied to the Connector
|
||||
// resources.
|
||||
mustUpdate(t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
conn.Spec.ProxyClass = "custom-metadata"
|
||||
})
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 3. ProxyClass is set to Ready by proxy-class reconciler. Connector
|
||||
// get reconciled and configuration from the ProxyClass is applied to
|
||||
// its resources.
|
||||
mustUpdateStatus(t, fc, "", "custom-metadata", func(pc *tsapi.ProxyClass) {
|
||||
pc.Status = tsapi.ProxyClassStatus{
|
||||
Conditions: []tsapi.ConnectorCondition{{
|
||||
Status: metav1.ConditionTrue,
|
||||
Type: tsapi.ProxyClassready,
|
||||
ObservedGeneration: pc.Generation,
|
||||
}}}
|
||||
})
|
||||
opts.proxyClass = pc.Name
|
||||
// We lose the auth key on second reconcile, because in code it's set to
|
||||
// StringData, but is actually read from Data. This works with a real
|
||||
// API server, but not with our test setup here.
|
||||
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 4. Connector.spec.proxyClass field is unset, Connector gets
|
||||
// reconciled and configuration from the ProxyClass is removed from the
|
||||
// cluster resources for the Connector.
|
||||
mustUpdate(t, fc, "", "test", func(conn *tsapi.Connector) {
|
||||
conn.Spec.ProxyClass = ""
|
||||
})
|
||||
opts.proxyClass = ""
|
||||
expectReconciled(t, cr, "", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
}
|
||||
|
||||
@@ -78,6 +78,10 @@ spec:
|
||||
- name: oauth
|
||||
mountPath: /oauth
|
||||
readOnly: true
|
||||
ports:
|
||||
- name: keyserver
|
||||
containerPort: 8443
|
||||
protocol: TCP
|
||||
{{- with .Values.operatorConfig.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: keyserver
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 8443
|
||||
protocol: TCP
|
||||
targetPort: 8443
|
||||
selector:
|
||||
app: operator
|
||||
type: ClusterIP
|
||||
@@ -22,7 +22,7 @@ rules:
|
||||
resources: ["ingressclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["tailscale.com"]
|
||||
resources: ["connectors", "connectors/status"]
|
||||
resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"]
|
||||
verbs: ["get", "list", "watch", "update"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@@ -64,3 +64,50 @@ roleRef:
|
||||
kind: Role
|
||||
name: operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: keyserver
|
||||
namespace: {{ .Release.Namespace }}
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get","list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: keyserver
|
||||
namespace: {{ .Release.Namespace }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: operator
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: keyserver
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: tailscale-keyserver
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: tailscale-keyserver
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: operator
|
||||
namespace: {{ .Release.Namespace }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: tailscale-keyserver
|
||||
rules:
|
||||
- apiGroups: ["authentication.k8s.io"]
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs: ["create"]
|
||||
|
||||
@@ -30,3 +30,32 @@ roleRef:
|
||||
kind: Role
|
||||
name: proxies
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: proxies-token
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- "serviceaccounts/token"
|
||||
- "serviceaccounts" # needed?
|
||||
verbs:
|
||||
- "create"
|
||||
- "get"
|
||||
resourceNames:
|
||||
- "proxies"
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: proxies-token
|
||||
namespace: {{ .Release.Namespace }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: proxies
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: proxies-token
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
@@ -54,6 +54,9 @@ spec:
|
||||
description: Hostname is the tailnet hostname that should be assigned to the Connector node. If unset, hostname defaults to <connector name>-connector. Hostname can contain lower case letters, numbers and dashes, it must not start or end with a dash and must be between 2 and 63 characters long.
|
||||
type: string
|
||||
pattern: ^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
|
||||
proxyClass:
|
||||
description: ProxyClass is the name of the ProxyClass custom resource that contains configuration options that should be applied to the resources created for this Connector. If unset, the operator will create resources with the default configuration.
|
||||
type: string
|
||||
subnetRouter:
|
||||
description: SubnetRouter defines subnet routes that the Connector node should expose to tailnet. If unset, none are exposed. https://tailscale.com/kb/1019/subnets/
|
||||
type: object
|
||||
|
||||
497
cmd/k8s-operator/deploy/crds/tailscale.com_proxyclasses.yaml
Normal file
497
cmd/k8s-operator/deploy/crds/tailscale.com_proxyclasses.yaml
Normal file
@@ -0,0 +1,497 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.13.0
|
||||
name: proxyclasses.tailscale.com
|
||||
spec:
|
||||
group: tailscale.com
|
||||
names:
|
||||
kind: ProxyClass
|
||||
listKind: ProxyClassList
|
||||
plural: proxyclasses
|
||||
singular: proxyclass
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Status of the ProxyClass.
|
||||
jsonPath: .status.conditions[?(@.type == "ProxyClassReady")].reason
|
||||
name: Status
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
type: object
|
||||
required:
|
||||
- statefulSet
|
||||
properties:
|
||||
statefulSet:
|
||||
description: Proxy's StatefulSet spec.
|
||||
type: object
|
||||
properties:
|
||||
annotations:
|
||||
description: Annotations that will be added to the StatefulSet created for the proxy. Any Annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
labels:
|
||||
description: Labels that will be added to the StatefulSet created for the proxy. Any labels specified here will be merged with the default labels applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other labels that might have been applied by other actors. Label keys and values must be valid Kubernetes label keys and values. https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
pod:
|
||||
description: Configuration for the proxy Pod.
|
||||
type: object
|
||||
properties:
|
||||
annotations:
|
||||
description: Annotations that will be added to the proxy Pod. Any annotations specified here will be merged with the default annotations applied to the Pod by the Tailscale Kubernetes operator. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
imagePullSecrets:
|
||||
description: Proxy Pod's image pull Secrets. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec
|
||||
type: array
|
||||
items:
|
||||
description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
x-kubernetes-map-type: atomic
|
||||
labels:
|
||||
description: Labels that will be added to the proxy Pod. Any labels specified here will be merged with the default labels applied to the Pod by the Tailscale Kubernetes operator. Label keys and values must be valid Kubernetes label keys and values. https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
nodeName:
|
||||
description: Proxy Pod's node name. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
type: string
|
||||
nodeSelector:
|
||||
description: Proxy Pod's node selector. By default Tailscale Kubernetes operator does not apply any node selector. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
securityContext:
|
||||
description: Proxy Pod's security context. By default Tailscale Kubernetes operator does not apply any Pod security context. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-2
|
||||
type: object
|
||||
properties:
|
||||
fsGroup:
|
||||
description: "A special supplemental group that applies to all containers in a pod. Some volume types allow the Kubelet to change the ownership of that volume to be owned by the pod: \n 1. The owning GID will be the FSGroup 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) 3. The permission bits are OR'd with rw-rw---- \n If unset, the Kubelet will not modify the ownership and permissions of any volume. Note that this field cannot be set when spec.os.name is windows."
|
||||
type: integer
|
||||
format: int64
|
||||
fsGroupChangePolicy:
|
||||
description: 'fsGroupChangePolicy defines behavior of changing ownership and permission of the volume before being exposed inside Pod. This field will only apply to volume types which support fsGroup based ownership(and permissions). It will have no effect on ephemeral volume types such as: secret, configmaps and emptydir. Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. Note that this field cannot be set when spec.os.name is windows.'
|
||||
type: string
|
||||
runAsGroup:
|
||||
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: integer
|
||||
format: int64
|
||||
runAsNonRoot:
|
||||
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: boolean
|
||||
runAsUser:
|
||||
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: integer
|
||||
format: int64
|
||||
seLinuxOptions:
|
||||
description: The SELinux context to be applied to all containers. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
properties:
|
||||
level:
|
||||
description: Level is SELinux level label that applies to the container.
|
||||
type: string
|
||||
role:
|
||||
description: Role is a SELinux role label that applies to the container.
|
||||
type: string
|
||||
type:
|
||||
description: Type is a SELinux type label that applies to the container.
|
||||
type: string
|
||||
user:
|
||||
description: User is a SELinux user label that applies to the container.
|
||||
type: string
|
||||
seccompProfile:
|
||||
description: The seccomp options to use by the containers in this pod. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
localhostProfile:
|
||||
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
|
||||
type: string
|
||||
supplementalGroups:
|
||||
description: A list of groups applied to the first process run in each container, in addition to the container's primary GID, the fsGroup (if specified), and group memberships defined in the container image for the uid of the container process. If unspecified, no additional groups are added to any container. Note that group memberships defined in the container image for the uid of the container process are still effective, even if they are not included in this list. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
sysctls:
|
||||
description: Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported sysctls (by the container runtime) might fail to launch. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: array
|
||||
items:
|
||||
description: Sysctl defines a kernel parameter to be set
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- value
|
||||
properties:
|
||||
name:
|
||||
description: Name of a property to set
|
||||
type: string
|
||||
value:
|
||||
description: Value of a property to set
|
||||
type: string
|
||||
windowsOptions:
|
||||
description: The Windows specific settings applied to all containers. If unspecified, the options within a container's SecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
|
||||
type: object
|
||||
properties:
|
||||
gmsaCredentialSpec:
|
||||
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
|
||||
type: string
|
||||
gmsaCredentialSpecName:
|
||||
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
|
||||
type: string
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: string
|
||||
tailscaleContainer:
|
||||
description: Configuration for the proxy container running tailscale.
|
||||
type: object
|
||||
properties:
|
||||
resources:
|
||||
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
|
||||
type: object
|
||||
properties:
|
||||
claims:
|
||||
description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers."
|
||||
type: array
|
||||
items:
|
||||
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
|
||||
type: string
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
limits:
|
||||
description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
additionalProperties:
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
x-kubernetes-int-or-string: true
|
||||
requests:
|
||||
description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
additionalProperties:
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
x-kubernetes-int-or-string: true
|
||||
securityContext:
|
||||
description: 'Container security context. Security context specified here will override the security context by the operator. By default the operator: - sets ''privileged: true'' for the init container - set NET_ADMIN capability for tailscale container for proxies that are created for Services or Connector. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context'
|
||||
type: object
|
||||
properties:
|
||||
allowPrivilegeEscalation:
|
||||
description: 'AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.'
|
||||
type: boolean
|
||||
capabilities:
|
||||
description: The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
properties:
|
||||
add:
|
||||
description: Added capabilities
|
||||
type: array
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
drop:
|
||||
description: Removed capabilities
|
||||
type: array
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
privileged:
|
||||
description: Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
procMount:
|
||||
description: procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: string
|
||||
readOnlyRootFilesystem:
|
||||
description: Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
runAsGroup:
|
||||
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: integer
|
||||
format: int64
|
||||
runAsNonRoot:
|
||||
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: boolean
|
||||
runAsUser:
|
||||
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: integer
|
||||
format: int64
|
||||
seLinuxOptions:
|
||||
description: The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
properties:
|
||||
level:
|
||||
description: Level is SELinux level label that applies to the container.
|
||||
type: string
|
||||
role:
|
||||
description: Role is a SELinux role label that applies to the container.
|
||||
type: string
|
||||
type:
|
||||
description: Type is a SELinux type label that applies to the container.
|
||||
type: string
|
||||
user:
|
||||
description: User is a SELinux user label that applies to the container.
|
||||
type: string
|
||||
seccompProfile:
|
||||
description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
localhostProfile:
|
||||
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
|
||||
type: string
|
||||
windowsOptions:
|
||||
description: The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
|
||||
type: object
|
||||
properties:
|
||||
gmsaCredentialSpec:
|
||||
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
|
||||
type: string
|
||||
gmsaCredentialSpecName:
|
||||
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
|
||||
type: string
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: string
|
||||
tailscaleInitContainer:
|
||||
description: Configuration for the proxy init container that enables forwarding.
|
||||
type: object
|
||||
properties:
|
||||
resources:
|
||||
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
|
||||
type: object
|
||||
properties:
|
||||
claims:
|
||||
description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers."
|
||||
type: array
|
||||
items:
|
||||
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
|
||||
type: string
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
limits:
|
||||
description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
additionalProperties:
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
x-kubernetes-int-or-string: true
|
||||
requests:
|
||||
description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
additionalProperties:
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
x-kubernetes-int-or-string: true
|
||||
securityContext:
|
||||
description: 'Container security context. Security context specified here will override the security context by the operator. By default the operator: - sets ''privileged: true'' for the init container - set NET_ADMIN capability for tailscale container for proxies that are created for Services or Connector. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context'
|
||||
type: object
|
||||
properties:
|
||||
allowPrivilegeEscalation:
|
||||
description: 'AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.'
|
||||
type: boolean
|
||||
capabilities:
|
||||
description: The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
properties:
|
||||
add:
|
||||
description: Added capabilities
|
||||
type: array
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
drop:
|
||||
description: Removed capabilities
|
||||
type: array
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
privileged:
|
||||
description: Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
procMount:
|
||||
description: procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: string
|
||||
readOnlyRootFilesystem:
|
||||
description: Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
runAsGroup:
|
||||
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: integer
|
||||
format: int64
|
||||
runAsNonRoot:
|
||||
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: boolean
|
||||
runAsUser:
|
||||
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: integer
|
||||
format: int64
|
||||
seLinuxOptions:
|
||||
description: The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
properties:
|
||||
level:
|
||||
description: Level is SELinux level label that applies to the container.
|
||||
type: string
|
||||
role:
|
||||
description: Role is a SELinux role label that applies to the container.
|
||||
type: string
|
||||
type:
|
||||
description: Type is a SELinux type label that applies to the container.
|
||||
type: string
|
||||
user:
|
||||
description: User is a SELinux user label that applies to the container.
|
||||
type: string
|
||||
seccompProfile:
|
||||
description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
localhostProfile:
|
||||
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
|
||||
type: string
|
||||
windowsOptions:
|
||||
description: The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
|
||||
type: object
|
||||
properties:
|
||||
gmsaCredentialSpec:
|
||||
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
|
||||
type: string
|
||||
gmsaCredentialSpecName:
|
||||
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
|
||||
type: string
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: string
|
||||
tolerations:
|
||||
description: Proxy Pod's tolerations. By default Tailscale Kubernetes operator does not apply any tolerations. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
type: array
|
||||
items:
|
||||
description: The pod this Toleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.
|
||||
type: object
|
||||
properties:
|
||||
effect:
|
||||
description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
|
||||
type: string
|
||||
key:
|
||||
description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.
|
||||
type: string
|
||||
operator:
|
||||
description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.
|
||||
type: integer
|
||||
format: int64
|
||||
value:
|
||||
description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
|
||||
type: string
|
||||
replicas:
|
||||
type: integer
|
||||
format: int32
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
conditions:
|
||||
description: List of status conditions to indicate the status of the ProxyClass. Known condition types are `ProxyClassReady`.
|
||||
type: array
|
||||
items:
|
||||
description: ConnectorCondition contains condition information for a Connector.
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
|
||||
type: string
|
||||
format: date-time
|
||||
message:
|
||||
description: Message is a human readable description of the details of the last transition, complementing reason.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
|
||||
type: integer
|
||||
format: int64
|
||||
reason:
|
||||
description: Reason is a brief machine readable explanation for the condition's last transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of ('True', 'False', 'Unknown').
|
||||
type: string
|
||||
type:
|
||||
description: Type of the condition, known values are (`SubnetRouterReady`).
|
||||
type: string
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
16
cmd/k8s-operator/deploy/examples/proxyclass.yaml
Normal file
16
cmd/k8s-operator/deploy/examples/proxyclass.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: tailscale.com/v1alpha1
|
||||
kind: ProxyClass
|
||||
metadata:
|
||||
name: prod
|
||||
spec:
|
||||
statefulSet:
|
||||
replicas: 2
|
||||
annotations:
|
||||
platform-component: infra
|
||||
pod:
|
||||
labels:
|
||||
team: eng
|
||||
nodeSelector:
|
||||
beta.kubernetes.io/os: "linux"
|
||||
imagePullSecrets:
|
||||
- name: "foo"
|
||||
@@ -79,6 +79,9 @@ spec:
|
||||
description: Hostname is the tailnet hostname that should be assigned to the Connector node. If unset, hostname defaults to <connector name>-connector. Hostname can contain lower case letters, numbers and dashes, it must not start or end with a dash and must be between 2 and 63 characters long.
|
||||
pattern: ^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
|
||||
type: string
|
||||
proxyClass:
|
||||
description: ProxyClass is the name of the ProxyClass custom resource that contains configuration options that should be applied to the resources created for this Connector. If unset, the operator will create resources with the default configuration.
|
||||
type: string
|
||||
subnetRouter:
|
||||
description: SubnetRouter defines subnet routes that the Connector node should expose to tailnet. If unset, none are exposed. https://tailscale.com/kb/1019/subnets/
|
||||
properties:
|
||||
@@ -153,6 +156,504 @@ spec:
|
||||
subresources:
|
||||
status: {}
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.13.0
|
||||
name: proxyclasses.tailscale.com
|
||||
spec:
|
||||
group: tailscale.com
|
||||
names:
|
||||
kind: ProxyClass
|
||||
listKind: ProxyClassList
|
||||
plural: proxyclasses
|
||||
singular: proxyclass
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Status of the ProxyClass.
|
||||
jsonPath: .status.conditions[?(@.type == "ProxyClassReady")].reason
|
||||
name: Status
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
statefulSet:
|
||||
description: Proxy's StatefulSet spec.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Annotations that will be added to the StatefulSet created for the proxy. Any Annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Labels that will be added to the StatefulSet created for the proxy. Any labels specified here will be merged with the default labels applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other labels that might have been applied by other actors. Label keys and values must be valid Kubernetes label keys and values. https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
|
||||
type: object
|
||||
pod:
|
||||
description: Configuration for the proxy Pod.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Annotations that will be added to the proxy Pod. Any annotations specified here will be merged with the default annotations applied to the Pod by the Tailscale Kubernetes operator. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
|
||||
type: object
|
||||
imagePullSecrets:
|
||||
description: Proxy Pod's image pull Secrets. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec
|
||||
items:
|
||||
description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
type: array
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Labels that will be added to the proxy Pod. Any labels specified here will be merged with the default labels applied to the Pod by the Tailscale Kubernetes operator. Label keys and values must be valid Kubernetes label keys and values. https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
|
||||
type: object
|
||||
nodeName:
|
||||
description: Proxy Pod's node name. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
type: string
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Proxy Pod's node selector. By default Tailscale Kubernetes operator does not apply any node selector. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
type: object
|
||||
securityContext:
|
||||
description: Proxy Pod's security context. By default Tailscale Kubernetes operator does not apply any Pod security context. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-2
|
||||
properties:
|
||||
fsGroup:
|
||||
description: "A special supplemental group that applies to all containers in a pod. Some volume types allow the Kubelet to change the ownership of that volume to be owned by the pod: \n 1. The owning GID will be the FSGroup 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) 3. The permission bits are OR'd with rw-rw---- \n If unset, the Kubelet will not modify the ownership and permissions of any volume. Note that this field cannot be set when spec.os.name is windows."
|
||||
format: int64
|
||||
type: integer
|
||||
fsGroupChangePolicy:
|
||||
description: 'fsGroupChangePolicy defines behavior of changing ownership and permission of the volume before being exposed inside Pod. This field will only apply to volume types which support fsGroup based ownership(and permissions). It will have no effect on ephemeral volume types such as: secret, configmaps and emptydir. Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. Note that this field cannot be set when spec.os.name is windows.'
|
||||
type: string
|
||||
runAsGroup:
|
||||
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
|
||||
format: int64
|
||||
type: integer
|
||||
runAsNonRoot:
|
||||
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: boolean
|
||||
runAsUser:
|
||||
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
|
||||
format: int64
|
||||
type: integer
|
||||
seLinuxOptions:
|
||||
description: The SELinux context to be applied to all containers. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
level:
|
||||
description: Level is SELinux level label that applies to the container.
|
||||
type: string
|
||||
role:
|
||||
description: Role is a SELinux role label that applies to the container.
|
||||
type: string
|
||||
type:
|
||||
description: Type is a SELinux type label that applies to the container.
|
||||
type: string
|
||||
user:
|
||||
description: User is a SELinux user label that applies to the container.
|
||||
type: string
|
||||
type: object
|
||||
seccompProfile:
|
||||
description: The seccomp options to use by the containers in this pod. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
localhostProfile:
|
||||
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
supplementalGroups:
|
||||
description: A list of groups applied to the first process run in each container, in addition to the container's primary GID, the fsGroup (if specified), and group memberships defined in the container image for the uid of the container process. If unspecified, no additional groups are added to any container. Note that group memberships defined in the container image for the uid of the container process are still effective, even if they are not included in this list. Note that this field cannot be set when spec.os.name is windows.
|
||||
items:
|
||||
format: int64
|
||||
type: integer
|
||||
type: array
|
||||
sysctls:
|
||||
description: Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported sysctls (by the container runtime) might fail to launch. Note that this field cannot be set when spec.os.name is windows.
|
||||
items:
|
||||
description: Sysctl defines a kernel parameter to be set
|
||||
properties:
|
||||
name:
|
||||
description: Name of a property to set
|
||||
type: string
|
||||
value:
|
||||
description: Value of a property to set
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
windowsOptions:
|
||||
description: The Windows specific settings applied to all containers. If unspecified, the options within a container's SecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
|
||||
properties:
|
||||
gmsaCredentialSpec:
|
||||
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
|
||||
type: string
|
||||
gmsaCredentialSpecName:
|
||||
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
|
||||
type: string
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
tailscaleContainer:
|
||||
description: Configuration for the proxy container running tailscale.
|
||||
properties:
|
||||
resources:
|
||||
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
|
||||
properties:
|
||||
claims:
|
||||
description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers."
|
||||
items:
|
||||
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||
properties:
|
||||
name:
|
||||
description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
limits:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
requests:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
securityContext:
|
||||
description: 'Container security context. Security context specified here will override the security context by the operator. By default the operator: - sets ''privileged: true'' for the init container - set NET_ADMIN capability for tailscale container for proxies that are created for Services or Connector. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context'
|
||||
properties:
|
||||
allowPrivilegeEscalation:
|
||||
description: 'AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.'
|
||||
type: boolean
|
||||
capabilities:
|
||||
description: The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
add:
|
||||
description: Added capabilities
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
type: array
|
||||
drop:
|
||||
description: Removed capabilities
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
privileged:
|
||||
description: Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
procMount:
|
||||
description: procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: string
|
||||
readOnlyRootFilesystem:
|
||||
description: Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
runAsGroup:
|
||||
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
format: int64
|
||||
type: integer
|
||||
runAsNonRoot:
|
||||
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: boolean
|
||||
runAsUser:
|
||||
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
format: int64
|
||||
type: integer
|
||||
seLinuxOptions:
|
||||
description: The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
level:
|
||||
description: Level is SELinux level label that applies to the container.
|
||||
type: string
|
||||
role:
|
||||
description: Role is a SELinux role label that applies to the container.
|
||||
type: string
|
||||
type:
|
||||
description: Type is a SELinux type label that applies to the container.
|
||||
type: string
|
||||
user:
|
||||
description: User is a SELinux user label that applies to the container.
|
||||
type: string
|
||||
type: object
|
||||
seccompProfile:
|
||||
description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
localhostProfile:
|
||||
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
windowsOptions:
|
||||
description: The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
|
||||
properties:
|
||||
gmsaCredentialSpec:
|
||||
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
|
||||
type: string
|
||||
gmsaCredentialSpecName:
|
||||
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
|
||||
type: string
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
tailscaleInitContainer:
|
||||
description: Configuration for the proxy init container that enables forwarding.
|
||||
properties:
|
||||
resources:
|
||||
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
|
||||
properties:
|
||||
claims:
|
||||
description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers."
|
||||
items:
|
||||
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||
properties:
|
||||
name:
|
||||
description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
limits:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
requests:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
|
||||
type: object
|
||||
type: object
|
||||
securityContext:
|
||||
description: 'Container security context. Security context specified here will override the security context by the operator. By default the operator: - sets ''privileged: true'' for the init container - set NET_ADMIN capability for tailscale container for proxies that are created for Services or Connector. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context'
|
||||
properties:
|
||||
allowPrivilegeEscalation:
|
||||
description: 'AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.'
|
||||
type: boolean
|
||||
capabilities:
|
||||
description: The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
add:
|
||||
description: Added capabilities
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
type: array
|
||||
drop:
|
||||
description: Removed capabilities
|
||||
items:
|
||||
description: Capability represent POSIX capabilities type
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
privileged:
|
||||
description: Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
procMount:
|
||||
description: procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: string
|
||||
readOnlyRootFilesystem:
|
||||
description: Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.
|
||||
type: boolean
|
||||
runAsGroup:
|
||||
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
format: int64
|
||||
type: integer
|
||||
runAsNonRoot:
|
||||
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: boolean
|
||||
runAsUser:
|
||||
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
format: int64
|
||||
type: integer
|
||||
seLinuxOptions:
|
||||
description: The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
level:
|
||||
description: Level is SELinux level label that applies to the container.
|
||||
type: string
|
||||
role:
|
||||
description: Role is a SELinux role label that applies to the container.
|
||||
type: string
|
||||
type:
|
||||
description: Type is a SELinux type label that applies to the container.
|
||||
type: string
|
||||
user:
|
||||
description: User is a SELinux user label that applies to the container.
|
||||
type: string
|
||||
type: object
|
||||
seccompProfile:
|
||||
description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.
|
||||
properties:
|
||||
localhostProfile:
|
||||
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
|
||||
type: string
|
||||
type:
|
||||
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
windowsOptions:
|
||||
description: The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
|
||||
properties:
|
||||
gmsaCredentialSpec:
|
||||
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
|
||||
type: string
|
||||
gmsaCredentialSpecName:
|
||||
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
|
||||
type: string
|
||||
hostProcess:
|
||||
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
|
||||
type: boolean
|
||||
runAsUserName:
|
||||
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
tolerations:
|
||||
description: Proxy Pod's tolerations. By default Tailscale Kubernetes operator does not apply any tolerations. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
items:
|
||||
description: The pod this Toleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.
|
||||
properties:
|
||||
effect:
|
||||
description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
|
||||
type: string
|
||||
key:
|
||||
description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.
|
||||
type: string
|
||||
operator:
|
||||
description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.
|
||||
format: int64
|
||||
type: integer
|
||||
value:
|
||||
description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
replicas:
|
||||
format: int32
|
||||
type: integer
|
||||
type: object
|
||||
required:
|
||||
- statefulSet
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
conditions:
|
||||
description: List of status conditions to indicate the status of the ProxyClass. Known condition types are `ProxyClassReady`.
|
||||
items:
|
||||
description: ConnectorCondition contains condition information for a Connector.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: Message is a human readable description of the details of the last transition, complementing reason.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
|
||||
format: int64
|
||||
type: integer
|
||||
reason:
|
||||
description: Reason is a brief machine readable explanation for the condition's last transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of ('True', 'False', 'Unknown').
|
||||
type: string
|
||||
type:
|
||||
description: Type of the condition, known values are (`SubnetRouterReady`).
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
@@ -186,6 +687,8 @@ rules:
|
||||
resources:
|
||||
- connectors
|
||||
- connectors/status
|
||||
- proxyclasses
|
||||
- proxyclasses/status
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
@@ -193,6 +696,34 @@ rules:
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: tailscale-keyserver
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: proxies-token
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resourceNames:
|
||||
- proxies
|
||||
resources:
|
||||
- serviceaccounts/token
|
||||
- serviceaccounts
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: tailscale-operator
|
||||
@@ -206,6 +737,19 @@ subjects:
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: tailscale-keyserver
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: tailscale-keyserver
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: operator
|
||||
@@ -226,6 +770,21 @@ rules:
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: keyserver
|
||||
namespace: tailscale
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
@@ -253,6 +812,20 @@ subjects:
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: keyserver
|
||||
namespace: tailscale
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: keyserver
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: operator
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
@@ -265,6 +838,34 @@ subjects:
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: proxies-token
|
||||
namespace: tailscale
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: proxies-token
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: proxies
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: keyserver
|
||||
namespace: tailscale
|
||||
spec:
|
||||
ports:
|
||||
- port: 8443
|
||||
protocol: TCP
|
||||
targetPort: 8443
|
||||
selector:
|
||||
app: operator
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -311,6 +912,10 @@ spec:
|
||||
image: tailscale/k8s-operator:unstable
|
||||
imagePullPolicy: Always
|
||||
name: operator
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
name: keyserver
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: /oauth
|
||||
name: oauth
|
||||
|
||||
@@ -30,6 +30,14 @@ spec:
|
||||
value: "false"
|
||||
- name: TS_AUTH_ONCE
|
||||
value: "true"
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
|
||||
@@ -22,3 +22,7 @@ spec:
|
||||
value: "true"
|
||||
- name: TS_AUTH_ONCE
|
||||
value: "true"
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
|
||||
@@ -19,10 +19,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
operatorDeploymentFilesPath = "cmd/k8s-operator/deploy"
|
||||
crdPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml"
|
||||
helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates"
|
||||
crdTemplatePath = helmTemplatesPath + "/connectors.yaml"
|
||||
operatorDeploymentFilesPath = "cmd/k8s-operator/deploy"
|
||||
connectorCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml"
|
||||
proxyClassCRDPath = operatorDeploymentFilesPath + "/crds/tailscale.com_proxyclasses.yaml"
|
||||
helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates"
|
||||
connectorCRDHelmTemplatePath = helmTemplatesPath + "/connector.yaml"
|
||||
proxyClassCRDHelmTemplatePath = helmTemplatesPath + "/proxyclass.yaml"
|
||||
|
||||
helmConditionalStart = "{{ if .Values.installCRDs -}}\n"
|
||||
helmConditionalEnd = "{{- end -}}"
|
||||
@@ -44,9 +46,9 @@ func main() {
|
||||
default:
|
||||
log.Fatalf("unknown option %s, known options are 'staticmanifests', 'helmcrd'", os.Args[1])
|
||||
}
|
||||
log.Printf("Inserting CRD into the Helm templates")
|
||||
log.Printf("Inserting CRDs Helm templates")
|
||||
if err := generate(repoRoot); err != nil {
|
||||
log.Fatalf("error adding Connector CRD to Helm templates: %v", err)
|
||||
log.Fatalf("error adding CRDs to Helm templates: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := cleanup(repoRoot); err != nil {
|
||||
@@ -106,34 +108,48 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// generate places tailscale.com CRDs (currently Connector and ProxyClass) into
|
||||
// the Helm chart templates behind .Values.installCRDs=true condition (true by
|
||||
// default).
|
||||
func generate(baseDir string) error {
|
||||
log.Print("Placing Connector CRD into Helm templates..")
|
||||
chartBytes, err := os.ReadFile(filepath.Join(baseDir, crdPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading CRD contents: %w", err)
|
||||
addCRDToHelm := func(crdPath, crdTemplatePath string) error {
|
||||
chartBytes, err := os.ReadFile(filepath.Join(baseDir, crdPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading CRD contents: %w", err)
|
||||
}
|
||||
// Place a new temporary Helm template file with the templated CRD
|
||||
// contents into Helm templates.
|
||||
file, err := os.Create(filepath.Join(baseDir, crdTemplatePath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating CRD template file: %w", err)
|
||||
}
|
||||
if _, err := file.Write([]byte(helmConditionalStart)); err != nil {
|
||||
return fmt.Errorf("error writing helm if statement start: %w", err)
|
||||
}
|
||||
if _, err := file.Write(chartBytes); err != nil {
|
||||
return fmt.Errorf("error writing chart bytes: %w", err)
|
||||
}
|
||||
if _, err := file.Write([]byte(helmConditionalEnd)); err != nil {
|
||||
return fmt.Errorf("error writing helm if-statement end: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Place a new temporary Helm template file with the templated CRD
|
||||
// contents into Helm templates.
|
||||
file, err := os.Create(filepath.Join(baseDir, crdTemplatePath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating CRD template file: %w", err)
|
||||
if err := addCRDToHelm(connectorCRDPath, connectorCRDHelmTemplatePath); err != nil {
|
||||
return fmt.Errorf("error adding Connector CRD to Helm templates: %w", err)
|
||||
}
|
||||
if _, err := file.Write([]byte(helmConditionalStart)); err != nil {
|
||||
return fmt.Errorf("error writing helm if statement start: %w", err)
|
||||
}
|
||||
if _, err := file.Write(chartBytes); err != nil {
|
||||
return fmt.Errorf("error writing chart bytes: %w", err)
|
||||
}
|
||||
if _, err := file.Write([]byte(helmConditionalEnd)); err != nil {
|
||||
return fmt.Errorf("error writing helm if-statement end: %w", err)
|
||||
if err := addCRDToHelm(proxyClassCRDPath, proxyClassCRDHelmTemplatePath); err != nil {
|
||||
return fmt.Errorf("error adding ProxyClass CRD to Helm templates: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(baseDir string) error {
|
||||
log.Print("Cleaning up CRD from Helm templates")
|
||||
if err := os.Remove(filepath.Join(baseDir, crdTemplatePath)); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error cleaning up CRD template: %w", err)
|
||||
if err := os.Remove(filepath.Join(baseDir, connectorCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error cleaning up Connector CRD template: %w", err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(baseDir, proxyClassCRDHelmTemplatePath)); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("error cleaning up ProxyClass CRD template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func Test_generate(t *testing.T) {
|
||||
t.Fatalf("Helm chart linter failed: %v", err)
|
||||
}
|
||||
|
||||
// Test that default Helm install contains the CRD
|
||||
// Test that default Helm install contains the Connector and ProxyClass CRDs.
|
||||
installContentsWithCRD := bytes.NewBuffer([]byte{})
|
||||
helmTemplateWithCRDCmd := exec.Command(helmCLIPath, "template", helmPackagePath)
|
||||
helmTemplateWithCRDCmd.Stderr = os.Stderr
|
||||
@@ -51,10 +51,13 @@ func Test_generate(t *testing.T) {
|
||||
t.Fatalf("templating Helm chart with CRDs failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(installContentsWithCRD.String(), "name: connectors.tailscale.com") {
|
||||
t.Errorf("CRD not found in default chart install")
|
||||
t.Errorf("Connector CRD not found in default chart install")
|
||||
}
|
||||
if !strings.Contains(installContentsWithCRD.String(), "name: proxyclasses.tailscale.com") {
|
||||
t.Errorf("ProxyClass CRD not found in default chart install")
|
||||
}
|
||||
|
||||
// Test that CRD can be excluded from Helm chart install
|
||||
// Test that CRDs can be excluded from Helm chart install
|
||||
installContentsWithoutCRD := bytes.NewBuffer([]byte{})
|
||||
helmTemplateWithoutCRDCmd := exec.Command(helmCLIPath, "template", helmPackagePath, "--set", "installCRDs=false")
|
||||
helmTemplateWithoutCRDCmd.Stderr = os.Stderr
|
||||
@@ -63,6 +66,9 @@ func Test_generate(t *testing.T) {
|
||||
t.Fatalf("templating Helm chart without CRDs failed: %v", err)
|
||||
}
|
||||
if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") {
|
||||
t.Errorf("CRD found in chart install that should not contain a CRD")
|
||||
t.Errorf("Connector CRD found in chart install that should not contain a CRD")
|
||||
}
|
||||
if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") {
|
||||
t.Errorf("ProxyClass CRD found in chart install that should not contain a CRD")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,17 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
return fmt.Errorf("failed to add finalizer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
proxyClass := proxyClassForObject(ing)
|
||||
if proxyClass != "" {
|
||||
if ready, err := proxyClassIsReady(ctx, proxyClass, a.Client); err != nil {
|
||||
return fmt.Errorf("error verifying ProxyClass for Ingress: %w", err)
|
||||
} else if !ready {
|
||||
logger.Infof("ProxyClass %s specified for the Ingress, but is not (yet) Ready, waiting..", proxyClass)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.managedIngresses.Add(ing.UID)
|
||||
gaugeIngressResources.Set(int64(a.managedIngresses.Len()))
|
||||
@@ -144,6 +155,16 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
// magic443 is a fake hostname that we can use to tell containerboot to swap
|
||||
// out with the real hostname once it's known.
|
||||
const magic443 = "${TS_CERT_DOMAIN}:443"
|
||||
|
||||
tlsHostname := ""
|
||||
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
|
||||
tlsHostname = ing.Spec.TLS[0].Hosts[0]
|
||||
}
|
||||
tlsHost := ipn.HostPort(fmt.Sprintf("%s:443", tlsHostname))
|
||||
if tlsHostname == "" {
|
||||
tlsHost = magic443
|
||||
}
|
||||
|
||||
sc := &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
@@ -151,18 +172,18 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
},
|
||||
},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
magic443: {
|
||||
tlsHost: {
|
||||
Handlers: map[string]*ipn.HTTPHandler{},
|
||||
},
|
||||
},
|
||||
}
|
||||
if opt.Bool(ing.Annotations[AnnotationFunnel]).EqualBool(true) {
|
||||
sc.AllowFunnel = map[ipn.HostPort]bool{
|
||||
magic443: true,
|
||||
tlsHost: true,
|
||||
}
|
||||
}
|
||||
|
||||
web := sc.Web[magic443]
|
||||
web := sc.Web[tlsHost]
|
||||
addIngressBackend := func(b *networkingv1.IngressBackend, path string) {
|
||||
if b == nil {
|
||||
return
|
||||
@@ -205,14 +226,10 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
}
|
||||
addIngressBackend(ing.Spec.DefaultBackend, "/")
|
||||
|
||||
var tlsHost string // hostname or FQDN or empty
|
||||
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
|
||||
tlsHost = ing.Spec.TLS[0].Hosts[0]
|
||||
}
|
||||
for _, rule := range ing.Spec.Rules {
|
||||
// Host is optional, but if it's present it must match the TLS host
|
||||
// otherwise we ignore the rule.
|
||||
if rule.Host != "" && rule.Host != tlsHost {
|
||||
if rule.Host != "" && rule.Host != tlsHostname {
|
||||
a.recorder.Eventf(ing, corev1.EventTypeWarning, "InvalidIngressBackend", "rule with host %q ignored, unsupported", rule.Host)
|
||||
continue
|
||||
}
|
||||
@@ -242,10 +259,9 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
tags = strings.Split(tstr, ",")
|
||||
}
|
||||
hostname := ing.Namespace + "-" + ing.Name + "-ingress"
|
||||
if tlsHost != "" {
|
||||
hostname, _, _ = strings.Cut(tlsHost, ".")
|
||||
}
|
||||
|
||||
// if tlsHost != "" {
|
||||
// hostname, _, _ = strings.Cut(tlsHost, ".")
|
||||
// }
|
||||
sts := &tailscaleSTSConfig{
|
||||
Hostname: hostname,
|
||||
ParentResourceName: ing.Name,
|
||||
@@ -253,6 +269,12 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
ServeConfig: sc,
|
||||
Tags: tags,
|
||||
ChildResourceLabels: crl,
|
||||
ProxyClass: proxyClass,
|
||||
TSVIP: a.tailnetVIPForIngress(ing),
|
||||
}
|
||||
|
||||
if val := ing.GetAnnotations()[AnnotationExperimentalForwardClusterTrafficViaL7IngresProxy]; val == "true" {
|
||||
sts.ForwardClusterTrafficViaL7IngressProxy = true
|
||||
}
|
||||
|
||||
if _, err := a.ssr.Provision(ctx, logger, sts); err != nil {
|
||||
@@ -291,6 +313,13 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *IngressReconciler) tailnetVIPForIngress(ing *networkingv1.Ingress) string {
|
||||
if !a.shouldExpose(ing) || ing.Annotations == nil {
|
||||
return ""
|
||||
}
|
||||
return ing.GetAnnotations()[AnnotationTSVIP]
|
||||
}
|
||||
|
||||
func (a *IngressReconciler) shouldExpose(ing *networkingv1.Ingress) bool {
|
||||
return ing != nil &&
|
||||
ing.Spec.IngressClassName != nil &&
|
||||
|
||||
270
cmd/k8s-operator/ingress_test.go
Normal file
270
cmd/k8s-operator/ingress_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"tailscale.com/ipn"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
func TestTailscaleIngress(t *testing.T) {
|
||||
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
|
||||
fc := fake.NewFakeClient(tsIngressClass)
|
||||
ft := &fakeTSClient{}
|
||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ingR := &IngressReconciler{
|
||||
Client: fc,
|
||||
ssr: &tailscaleSTSReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
tsnetServer: fakeTsnetServer,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
},
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// 1. Resources get created for regular Ingress
|
||||
ing := &networkingv1.Ingress{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: ptr.To("tailscale"),
|
||||
DefaultBackend: &networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "test",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{Hosts: []string{"default-test"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
mustCreate(t, fc, ing)
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "1.2.3.4",
|
||||
Ports: []corev1.ServicePort{{
|
||||
Port: 8080,
|
||||
Name: "http"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test", "ingress")
|
||||
opts := configOpts{
|
||||
stsName: shortName,
|
||||
secretName: fullName,
|
||||
namespace: "default",
|
||||
parentType: "ingress",
|
||||
hostname: "default-test",
|
||||
}
|
||||
serveConfig := &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{"${TS_CERT_DOMAIN}:443": {Handlers: map[string]*ipn.HTTPHandler{"/": {Proxy: "http://1.2.3.4:8080/"}}}},
|
||||
}
|
||||
opts.serveConfig = serveConfig
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
|
||||
|
||||
// 2. Ingress status gets updated with ingress proxy's MagicDNS name
|
||||
// once that becomes available.
|
||||
mustUpdate(t, fc, "operator-ns", opts.secretName, func(secret *corev1.Secret) {
|
||||
mak.Set(&secret.Data, "device_id", []byte("1234"))
|
||||
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
|
||||
})
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
|
||||
ing.Status.LoadBalancer = networkingv1.IngressLoadBalancerStatus{
|
||||
Ingress: []networkingv1.IngressLoadBalancerIngress{
|
||||
{Hostname: "foo.tailnetxyz.ts.net", Ports: []networkingv1.IngressPortStatus{{Port: 443, Protocol: "TCP"}}},
|
||||
},
|
||||
}
|
||||
expectEqual(t, fc, ing)
|
||||
|
||||
// 3. Resources get created for Ingress that should allow forwarding
|
||||
// cluster traffic
|
||||
mustUpdate(t, fc, "default", "test", func(ing *networkingv1.Ingress) {
|
||||
mak.Set(&ing.ObjectMeta.Annotations, AnnotationExperimentalForwardClusterTrafficViaL7IngresProxy, "true")
|
||||
})
|
||||
opts.shouldEnableForwardingClusterTrafficViaIngress = true
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 4. Resources get cleaned up when Ingress class is unset
|
||||
mustUpdate(t, fc, "default", "test", func(ing *networkingv1.Ingress) {
|
||||
ing.Spec.IngressClassName = ptr.To("nginx")
|
||||
})
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
expectReconciled(t, ingR, "default", "test") // deleting Ingress STS requires two reconciles
|
||||
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
|
||||
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
|
||||
}
|
||||
|
||||
func TestTailscaleIngressWithProxyClass(t *testing.T) {
|
||||
// Setup
|
||||
pc := &tsapi.ProxyClass{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "custom-metadata"},
|
||||
Spec: tsapi.ProxyClassSpec{StatefulSet: &tsapi.StatefulSet{
|
||||
Labels: map[string]string{"foo": "bar"},
|
||||
Annotations: map[string]string{"bar.io/foo": "some-val"},
|
||||
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
|
||||
}
|
||||
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(pc, tsIngressClass).
|
||||
WithStatusSubresource(pc).
|
||||
Build()
|
||||
ft := &fakeTSClient{}
|
||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ingR := &IngressReconciler{
|
||||
Client: fc,
|
||||
ssr: &tailscaleSTSReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
tsnetServer: fakeTsnetServer,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
},
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// 1. Ingress is created with no ProxyClass specified, default proxy
|
||||
// resources get configured.
|
||||
ing := &networkingv1.Ingress{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: ptr.To("tailscale"),
|
||||
DefaultBackend: &networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "test",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{Hosts: []string{"default-test"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
mustCreate(t, fc, ing)
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "1.2.3.4",
|
||||
Ports: []corev1.ServicePort{{
|
||||
Port: 8080,
|
||||
Name: "http"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test", "ingress")
|
||||
opts := configOpts{
|
||||
stsName: shortName,
|
||||
secretName: fullName,
|
||||
namespace: "default",
|
||||
parentType: "ingress",
|
||||
hostname: "default-test",
|
||||
}
|
||||
serveConfig := &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{"${TS_CERT_DOMAIN}:443": {Handlers: map[string]*ipn.HTTPHandler{"/": {Proxy: "http://1.2.3.4:8080/"}}}},
|
||||
}
|
||||
opts.serveConfig = serveConfig
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
|
||||
|
||||
// 2. Ingress is updated to specify a ProxyClass, ProxyClass is not yet
|
||||
// ready, so proxy resource configuration does not change.
|
||||
mustUpdate(t, fc, "default", "test", func(ing *networkingv1.Ingress) {
|
||||
mak.Set(&ing.ObjectMeta.Labels, LabelProxyClass, "custom-metadata")
|
||||
})
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
|
||||
|
||||
// 3. ProxyClass is set to Ready by proxy-class reconciler. Ingress get
|
||||
// reconciled and configuration from the ProxyClass is applied to the
|
||||
// created proxy resources.
|
||||
mustUpdateStatus(t, fc, "", "custom-metadata", func(pc *tsapi.ProxyClass) {
|
||||
pc.Status = tsapi.ProxyClassStatus{
|
||||
Conditions: []tsapi.ConnectorCondition{{
|
||||
Status: metav1.ConditionTrue,
|
||||
Type: tsapi.ProxyClassready,
|
||||
ObservedGeneration: pc.Generation,
|
||||
}}}
|
||||
})
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
opts.proxyClass = pc.Name
|
||||
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
|
||||
|
||||
// 4. tailscale.com/proxy-class label is removed from the Ingress, the
|
||||
// Ingress gets reconciled and the custom ProxyClass configuration is
|
||||
// removed from the proxy resources.
|
||||
mustUpdate(t, fc, "default", "test", func(ing *networkingv1.Ingress) {
|
||||
delete(ing.ObjectMeta.Labels, LabelProxyClass)
|
||||
})
|
||||
expectReconciled(t, ingR, "default", "test")
|
||||
opts.proxyClass = ""
|
||||
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
|
||||
}
|
||||
244
cmd/k8s-operator/keyserver.go
Normal file
244
cmd/k8s-operator/keyserver.go
Normal file
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
authv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"tailscale.com/client/tailscale"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
)
|
||||
|
||||
type keyServer struct {
|
||||
restConfig *rest.Config
|
||||
client.Client
|
||||
logger *zap.SugaredLogger
|
||||
tsNamespace string
|
||||
defaultDeviceTags []string
|
||||
tsClient tsClient
|
||||
}
|
||||
|
||||
func (ks *keyServer) runKeyServer() error {
|
||||
proxyServiceAccountName := fmt.Sprintf("system:serviceaccount:%s:proxies", ks.tsNamespace)
|
||||
// create a client-go client as c/r client cannot be used to directly
|
||||
// access Auth interface to create TokenReviews. TokenReviews are not
|
||||
// objects that exist in cluster, so the normal c/r flow of 'CREATE and
|
||||
// object, if needed to observe its current state GET it does not work
|
||||
// here- we need to read the status from the TokenReview status as
|
||||
// returned in response, so we need to use the actual auth client.
|
||||
// TODO: maybe I actually don't need to do this because the object
|
||||
// passed to c/r Create would get updated?
|
||||
kubeClient, err := kubernetes.NewForConfig(ks.restConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating a new kube client: %v", err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/keys", func(w http.ResponseWriter, r *http.Request) {
|
||||
ks.logger.Debugf("received request for an auth key")
|
||||
// Get the auth token - like https://github.com/kubernetes/apiserver/blob/release-1.29/pkg/authentication/request/bearertoken/bearertoken.go#L42-L63
|
||||
auth := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
if auth == "" {
|
||||
ks.logger.Info("received a request with no auth header")
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 3)
|
||||
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
ks.logger.Info("received a request with no bearer token")
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
|
||||
// Empty bearer tokens aren't valid
|
||||
if len(token) == 0 {
|
||||
ks.logger.Info("received a request with an empty bearer token")
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// create a TokenReview
|
||||
tr := &authv1.TokenReview{
|
||||
Spec: authv1.TokenReviewSpec{Token: token, Audiences: []string{"ts-keyserver"}},
|
||||
}
|
||||
|
||||
// TODO: alt would be to delegate via auth webhook - that's how
|
||||
// RBAC proxy does it. Compare.
|
||||
resp, err := kubeClient.AuthenticationV1().TokenReviews().Create(r.Context(), tr, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
ks.logger.Errorf("error creating a TokenReview: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !resp.Status.Authenticated {
|
||||
ks.logger.Info("token was not authenticated")
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// TODO: set and validate audience
|
||||
// We know that only ServiceAccount 'proxies' in operator
|
||||
// namespace should be allowed to call 'keys' endpoint.
|
||||
// Alternatively we could assign 'proxies' an RBAC allowing it
|
||||
// to call '/keys' endpoint (RBAC for non-resource URLs). At the
|
||||
// moment I don't see a value in doing that as we know what
|
||||
// ServiceAccount is allowed to perform the action and an
|
||||
// operator installation always includes this ServiceAccount.
|
||||
if username := resp.Status.User.Username; username != proxyServiceAccountName {
|
||||
ks.logger.Info("received a request for token for user %s, expected %s", username, proxyServiceAccountName)
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// TODO: ensure that this will always have extras when the token is sent from containerboot
|
||||
if resp.Status.User.Extra == nil {
|
||||
ks.logger.Info("received a request for a token that does not contain extra information, please report this")
|
||||
http.Error(w, "unable to identify caller Pod", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if len(resp.Status.User.Extra[serviceaccount.PodNameKey]) != 1 || resp.Status.User.Extra[serviceaccount.PodNameKey][0] == "" {
|
||||
ks.logger.Infof("impossible to identify caller Pod from token review response: %#+v", resp.Status.User.Extra[serviceaccount.PodNameKey])
|
||||
http.Error(w, "unable to identify caller Pod", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
podName := types.NamespacedName{Namespace: ks.tsNamespace, Name: resp.Status.User.Extra[serviceaccount.PodNameKey][0]}
|
||||
ks.logger.Debugf("request for key authenticated as from Pod %s", podName)
|
||||
|
||||
// TODO: cache metadata only for these, filter ts namespace and labels
|
||||
pod := &corev1.Pod{}
|
||||
// TODO: is it right to use this context?
|
||||
if err := ks.Client.Get(r.Context(), podName, pod); err != nil {
|
||||
ks.logger.Errorf("unable to retrieve caller Pod from cache: %v", err)
|
||||
http.Error(w, "unable to identify caller Pod", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// Get the parent resource and figure out what tags are needed.
|
||||
// Alternatives could be 1) annotate Pods with the desired ACL
|
||||
// tags 2) pass each StatefulSet a specific URL that includes
|
||||
// the tags (i.e base64 encoded). But 2) would probably require
|
||||
// RBAC for calling _that_ URL (and we currently use the same
|
||||
// ServiceAccount for all proxies). 1) could be ok (and would
|
||||
// also solve the problem where user updating ACL tags is not
|
||||
// picked up by proxies), but should discuss the model
|
||||
// (including what should happen when ACL tags are updated).
|
||||
// Generally of course should speed this up much as possible.
|
||||
tags, err := ks.tagsForPod(r.Context(), pod)
|
||||
if err != nil {
|
||||
ks.logger.Errorf("error determining ACL tags to apply to the auth key: %v", err)
|
||||
http.Error(w, "error determining ACL tags", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// create the device
|
||||
// TODO: bump a metric here. probably should also be user facing?
|
||||
key, err := ks.newAuthKey(r.Context(), tags)
|
||||
if err != nil {
|
||||
ks.logger.Errorf("error determining ACL tags to apply to the auth key")
|
||||
http.Error(w, "error creating a new auth key", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
// probably?
|
||||
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
|
||||
w.Write([]byte(key))
|
||||
})
|
||||
srv := http.Server{
|
||||
Handler: mux,
|
||||
Addr: ":8443", // 443 is auth proxy if that's too running on this operator instance
|
||||
}
|
||||
ks.logger.Infof("running key server on %v", srv.Addr)
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
func (ks *keyServer) newAuthKey(ctx context.Context, tags []string) (string, error) {
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
Create: tailscale.KeyDeviceCreateCapabilities{
|
||||
Reusable: false,
|
||||
Preauthorized: true,
|
||||
Tags: tags,
|
||||
},
|
||||
},
|
||||
}
|
||||
key, _, err := ks.tsClient.CreateKey(ctx, caps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (ks *keyServer) tagsForPod(ctx context.Context, pod *corev1.Pod) ([]string, error) {
|
||||
parentLabels, err := managedLabelsFromPod(pod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error determining parent resource labels: %v", err)
|
||||
}
|
||||
tags, err := ks.aclTagsForResource(ctx, parentLabels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error determining ACl tags: %v", err)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (ks *keyServer) aclTagsForResource(ctx context.Context, labels map[string]string) ([]string, error) {
|
||||
switch labels[LabelParentType] {
|
||||
case "svc":
|
||||
svcName := types.NamespacedName{Namespace: labels[LabelParentNamespace], Name: labels[LabelParentName]}
|
||||
svc := &corev1.Service{}
|
||||
if err := ks.Get(ctx, svcName, svc); err != nil {
|
||||
return nil, fmt.Errorf("error getting Service: %v", err)
|
||||
}
|
||||
return ks.aclsForObjectAnnotations(svc.Annotations), nil
|
||||
case "ingress":
|
||||
ingName := types.NamespacedName{Namespace: labels[LabelParentNamespace], Name: labels[LabelParentName]}
|
||||
ing := &networkingv1.Ingress{}
|
||||
if err := ks.Get(ctx, ingName, ing); err != nil {
|
||||
return nil, fmt.Errorf("error getting Ingress: %v", err)
|
||||
}
|
||||
return ks.aclsForObjectAnnotations(ing.Annotations), nil
|
||||
case "connector":
|
||||
connectorName := types.NamespacedName{Name: labels[LabelParentName]}
|
||||
conn := &tsapi.Connector{}
|
||||
if err := ks.Get(ctx, connectorName, conn); err != nil {
|
||||
return nil, fmt.Errorf("error getting Connector: %v", err)
|
||||
}
|
||||
if len(conn.Spec.Tags) > 0 {
|
||||
return conn.Spec.Tags.Stringify(), nil
|
||||
}
|
||||
return ks.defaultDeviceTags, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unkown parent type: %s", labels[LabelParentType])
|
||||
}
|
||||
}
|
||||
|
||||
func (ks *keyServer) aclsForObjectAnnotations(annots map[string]string) []string {
|
||||
if annots == nil || annots[AnnotationTags] == "" {
|
||||
return ks.defaultDeviceTags
|
||||
}
|
||||
return strings.Split(annots[AnnotationTags], ",")
|
||||
}
|
||||
|
||||
func managedLabelsFromPod(pod *corev1.Pod) (map[string]string, error) {
|
||||
labels := make(map[string]string)
|
||||
for _, labelName := range []string{LabelManaged, LabelParentName, LabelParentNamespace, LabelParentType} {
|
||||
if labelVal := pod.GetLabels()[labelName]; labelVal == "" {
|
||||
return nil, fmt.Errorf("Pod does not have label: %s", labelName)
|
||||
} else {
|
||||
labels[labelName] = labelVal
|
||||
}
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
@@ -222,6 +222,8 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
ByObject: map[client.Object]cache.ByObject{
|
||||
&corev1.Secret{}: nsFilter,
|
||||
&appsv1.StatefulSet{}: nsFilter,
|
||||
// TODO (irberkrm): cahce metadata only for Pods
|
||||
&corev1.Pod{}: nsFilter,
|
||||
},
|
||||
},
|
||||
Scheme: tsapi.GlobalScheme,
|
||||
@@ -233,6 +235,9 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
|
||||
svcFilter := handler.EnqueueRequestsFromMapFunc(serviceHandler)
|
||||
svcChildFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("svc"))
|
||||
// If a ProxyClassChanges, enqueue all Services labeled with that
|
||||
// ProxyClass's name.
|
||||
proxyClassFilterForSvc := handler.EnqueueRequestsFromMapFunc(proxyClassHandlerForSvc(mgr.GetClient(), startlog))
|
||||
|
||||
eventRecorder := mgr.GetEventRecorderFor("tailscale-operator")
|
||||
ssr := &tailscaleSTSReconciler{
|
||||
@@ -251,6 +256,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
Watches(&corev1.Service{}, svcFilter).
|
||||
Watches(&appsv1.StatefulSet{}, svcChildFilter).
|
||||
Watches(&corev1.Secret{}, svcChildFilter).
|
||||
Watches(&tsapi.ProxyClass{}, proxyClassFilterForSvc).
|
||||
Complete(&ServiceReconciler{
|
||||
ssr: ssr,
|
||||
Client: mgr.GetClient(),
|
||||
@@ -259,15 +265,19 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
recorder: eventRecorder,
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create controller: %v", err)
|
||||
startlog.Fatalf("could not create service reconciler: %v", err)
|
||||
}
|
||||
ingressChildFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("ingress"))
|
||||
// If a ProxyClassChanges, enqueue all Ingresses labeled with that
|
||||
// ProxyClass's name.
|
||||
proxyClassFilterForIngress := handler.EnqueueRequestsFromMapFunc(proxyClassHandlerForIngress(mgr.GetClient(), startlog))
|
||||
err = builder.
|
||||
ControllerManagedBy(mgr).
|
||||
For(&networkingv1.Ingress{}).
|
||||
Watches(&appsv1.StatefulSet{}, ingressChildFilter).
|
||||
Watches(&corev1.Secret{}, ingressChildFilter).
|
||||
Watches(&corev1.Service{}, ingressChildFilter).
|
||||
Watches(&tsapi.ProxyClass{}, proxyClassFilterForIngress).
|
||||
Complete(&IngressReconciler{
|
||||
ssr: ssr,
|
||||
recorder: eventRecorder,
|
||||
@@ -275,14 +285,18 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
logger: zlog.Named("ingress-reconciler"),
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create controller: %v", err)
|
||||
startlog.Fatalf("could not create ingress reconciler: %v", err)
|
||||
}
|
||||
|
||||
connectorFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("connector"))
|
||||
// If a ProxyClassChanges, enqueue all Connectors that have
|
||||
// .spec.proxyClass set to the name of this ProxyClass.
|
||||
proxyClassFilterForConnector := handler.EnqueueRequestsFromMapFunc(proxyClassHandlerForConnector(mgr.GetClient(), startlog))
|
||||
err = builder.ControllerManagedBy(mgr).
|
||||
For(&tsapi.Connector{}).
|
||||
Watches(&appsv1.StatefulSet{}, connectorFilter).
|
||||
Watches(&corev1.Secret{}, connectorFilter).
|
||||
Watches(&tsapi.ProxyClass{}, proxyClassFilterForConnector).
|
||||
Complete(&ConnectorReconciler{
|
||||
ssr: ssr,
|
||||
recorder: eventRecorder,
|
||||
@@ -293,6 +307,27 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
if err != nil {
|
||||
startlog.Fatal("could not create connector reconciler: %v", err)
|
||||
}
|
||||
err = builder.ControllerManagedBy(mgr).
|
||||
For(&tsapi.ProxyClass{}).
|
||||
Complete(&ProxyClassReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
recorder: eventRecorder,
|
||||
logger: zlog.Named("proxyclass-reconciler"),
|
||||
clock: tstime.DefaultClock{},
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatal("could not create proxyclass reconciler: %v", err)
|
||||
}
|
||||
// TODO: maybe put in a better place, but this needs rest config and c/r client
|
||||
ks := &keyServer{
|
||||
Client: mgr.GetClient(),
|
||||
restConfig: mgr.GetConfig(),
|
||||
logger: zlog.Named("keyserver"),
|
||||
tsNamespace: tsNamespace,
|
||||
defaultDeviceTags: strings.Split(tags, ","), // or do differently
|
||||
tsClient: tsClient,
|
||||
}
|
||||
go ks.runKeyServer()
|
||||
startlog.Infof("Startup complete, operator running, version: %s", version.Long())
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
startlog.Fatalf("could not start manager: %v", err)
|
||||
@@ -321,6 +356,7 @@ func parentFromObjectLabels(o client.Object) types.NamespacedName {
|
||||
Name: ls[LabelParentName],
|
||||
}
|
||||
}
|
||||
|
||||
func managedResourceHandlerForType(typ string) handler.MapFunc {
|
||||
return func(_ context.Context, o client.Object) []reconcile.Request {
|
||||
if !isManagedByType(o, typ) {
|
||||
@@ -330,14 +366,75 @@ func managedResourceHandlerForType(typ string) handler.MapFunc {
|
||||
{NamespacedName: parentFromObjectLabels(o)},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proxyClassHandlerForSvc returns a handler that, for a given ProxyClass,
|
||||
// returns a list of reconcile requests for all Services labeled with
|
||||
// tailscale.com/proxy-class: <proxy class name>.
|
||||
func proxyClassHandlerForSvc(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
||||
return func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
svcList := new(corev1.ServiceList)
|
||||
labels := map[string]string{
|
||||
LabelProxyClass: o.GetName(),
|
||||
}
|
||||
if err := cl.List(ctx, svcList, client.MatchingLabels(labels)); err != nil {
|
||||
logger.Debugf("error listing Services for ProxyClass: %v", err)
|
||||
return nil
|
||||
}
|
||||
reqs := make([]reconcile.Request, 0)
|
||||
for _, svc := range svcList.Items {
|
||||
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&svc)})
|
||||
}
|
||||
return reqs
|
||||
}
|
||||
}
|
||||
|
||||
// proxyClassHandlerForIngress returns a handler that, for a given ProxyClass,
|
||||
// returns a list of reconcile requests for all Ingresses labeled with
|
||||
// tailscale.com/proxy-class: <proxy class name>.
|
||||
func proxyClassHandlerForIngress(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
||||
return func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
ingList := new(networkingv1.IngressList)
|
||||
labels := map[string]string{
|
||||
LabelProxyClass: o.GetName(),
|
||||
}
|
||||
if err := cl.List(ctx, ingList, client.MatchingLabels(labels)); err != nil {
|
||||
logger.Debugf("error listing Ingresses for ProxyClass: %v", err)
|
||||
return nil
|
||||
}
|
||||
reqs := make([]reconcile.Request, 0)
|
||||
for _, ing := range ingList.Items {
|
||||
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&ing)})
|
||||
}
|
||||
return reqs
|
||||
}
|
||||
}
|
||||
|
||||
// proxyClassHandlerForConnector returns a handler that, for a given ProxyClass,
|
||||
// returns a list of reconcile requests for all Connectors that have
|
||||
// .spec.proxyClass set.
|
||||
func proxyClassHandlerForConnector(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
||||
return func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
connList := new(tsapi.ConnectorList)
|
||||
if err := cl.List(ctx, connList); err != nil {
|
||||
logger.Debugf("error listing Connectors for ProxyClass: %v", err)
|
||||
return nil
|
||||
}
|
||||
reqs := make([]reconcile.Request, 0)
|
||||
proxyClassName := o.GetName()
|
||||
for _, conn := range connList.Items {
|
||||
if conn.Spec.ProxyClass == proxyClassName {
|
||||
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&conn)})
|
||||
}
|
||||
}
|
||||
return reqs
|
||||
}
|
||||
}
|
||||
|
||||
func serviceHandler(_ context.Context, o client.Object) []reconcile.Request {
|
||||
if isManagedByType(o, "svc") {
|
||||
// If this is a Service managed by a Service we want to enqueue its parent
|
||||
return []reconcile.Request{{NamespacedName: parentFromObjectLabels(o)}}
|
||||
|
||||
}
|
||||
if isManagedResource(o) {
|
||||
// If this is a Servce managed by a resource that is not a Service, we leave it alone
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
func TestLoadBalancerClass(t *testing.T) {
|
||||
@@ -68,8 +70,8 @@ func TestLoadBalancerClass(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(opts))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
@@ -209,8 +211,8 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -233,8 +235,8 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
|
||||
// Change the tailscale-target-fqdn annotation which should update the
|
||||
// StatefulSet
|
||||
@@ -319,8 +321,8 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -343,8 +345,8 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
|
||||
}
|
||||
expectEqual(t, fc, want)
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
|
||||
// Change the tailscale-target-ip annotation which should update the
|
||||
// StatefulSet
|
||||
@@ -426,8 +428,8 @@ func TestAnnotations(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -534,8 +536,8 @@ func TestAnnotationIntoLB(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, since it would have normally happened at
|
||||
@@ -579,8 +581,8 @@ func TestAnnotationIntoLB(t *testing.T) {
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
// None of the proxy machinery should have changed...
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
// ... but the service should have a LoadBalancer status.
|
||||
|
||||
want = &corev1.Service{
|
||||
@@ -665,8 +667,8 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
|
||||
// Normally the Tailscale proxy pod would come up here and write its info
|
||||
// into the secret. Simulate that, then verify reconcile again and verify
|
||||
@@ -728,8 +730,8 @@ func TestLBIntoAnnotation(t *testing.T) {
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
|
||||
want = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -806,8 +808,8 @@ func TestCustomHostname(t *testing.T) {
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSecret(t, o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
want := &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -920,7 +922,104 @@ func TestCustomPriorityClassName(t *testing.T) {
|
||||
clusterTargetIP: "10.20.30.40",
|
||||
}
|
||||
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
}
|
||||
|
||||
func TestProxyClassForService(t *testing.T) {
|
||||
// Setup
|
||||
pc := &tsapi.ProxyClass{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "custom-metadata"},
|
||||
Spec: tsapi.ProxyClassSpec{StatefulSet: &tsapi.StatefulSet{
|
||||
Labels: map[string]string{"foo": "bar"},
|
||||
Annotations: map[string]string{"bar.io/foo": "some-val"},
|
||||
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
|
||||
}
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(pc).
|
||||
WithStatusSubresource(pc).
|
||||
Build()
|
||||
ft := &fakeTSClient{}
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sr := &ServiceReconciler{
|
||||
Client: fc,
|
||||
ssr: &tailscaleSTSReconciler{
|
||||
Client: fc,
|
||||
tsClient: ft,
|
||||
defaultTags: []string{"tag:k8s"},
|
||||
operatorNamespace: "operator-ns",
|
||||
proxyImage: "tailscale/tailscale",
|
||||
},
|
||||
logger: zl.Sugar(),
|
||||
}
|
||||
|
||||
// 1. A new tailscale LoadBalancer Service is created without any
|
||||
// ProxyClass. Resources get created for it as usual.
|
||||
mustCreate(t, fc, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.20.30.40",
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: ptr.To("tailscale"),
|
||||
},
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
|
||||
opts := configOpts{
|
||||
stsName: shortName,
|
||||
secretName: fullName,
|
||||
namespace: "default",
|
||||
parentType: "svc",
|
||||
hostname: "default-test",
|
||||
clusterTargetIP: "10.20.30.40",
|
||||
}
|
||||
expectEqual(t, fc, expectedSecret(t, opts))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 2. The Service gets updated with tailscale.com/proxy-class label
|
||||
// pointing at the 'custom-metadata' ProxyClass. The ProxyClass is not
|
||||
// yet ready, so no changes are actually applied to the proxy resources.
|
||||
mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
|
||||
mak.Set(&svc.Labels, LabelProxyClass, "custom-metadata")
|
||||
})
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 3. ProxyClass is set to Ready, the Service gets reconciled by the
|
||||
// services-reconciler and the customization from the ProxyClass is
|
||||
// applied to the proxy resources.
|
||||
mustUpdateStatus(t, fc, "", "custom-metadata", func(pc *tsapi.ProxyClass) {
|
||||
pc.Status = tsapi.ProxyClassStatus{
|
||||
Conditions: []tsapi.ConnectorCondition{{
|
||||
Status: metav1.ConditionTrue,
|
||||
Type: tsapi.ProxyClassready,
|
||||
ObservedGeneration: pc.Generation,
|
||||
}}}
|
||||
})
|
||||
opts.proxyClass = pc.Name
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
|
||||
// 4. tailscale.com/proxy-class label is removed from the Service, the
|
||||
// configuration from the ProxyClass is removed from the cluster
|
||||
// resources.
|
||||
mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
|
||||
delete(svc.Labels, LabelProxyClass)
|
||||
})
|
||||
opts.proxyClass = ""
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, opts))
|
||||
}
|
||||
|
||||
func TestDefaultLoadBalancer(t *testing.T) {
|
||||
@@ -964,7 +1063,7 @@ func TestDefaultLoadBalancer(t *testing.T) {
|
||||
|
||||
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
|
||||
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName))
|
||||
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
|
||||
o := configOpts{
|
||||
stsName: shortName,
|
||||
secretName: fullName,
|
||||
@@ -973,7 +1072,7 @@ func TestDefaultLoadBalancer(t *testing.T) {
|
||||
hostname: "default-test",
|
||||
clusterTargetIP: "10.20.30.40",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
}
|
||||
|
||||
func TestProxyFirewallMode(t *testing.T) {
|
||||
@@ -1026,7 +1125,7 @@ func TestProxyFirewallMode(t *testing.T) {
|
||||
firewallMode: "nftables",
|
||||
clusterTargetIP: "10.20.30.40",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(o))
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o))
|
||||
|
||||
}
|
||||
|
||||
|
||||
108
cmd/k8s-operator/proxyclass.go
Normal file
108
cmd/k8s-operator/proxyclass.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
||||
// cluster to your Tailnet.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
const (
|
||||
reasonProxyClassInvalid = "ProxyClassInvalid"
|
||||
reasonProxyClassValid = "ProxyClassValid"
|
||||
messageProxyClassInvalid = "ProxyClass is not valid: %v"
|
||||
)
|
||||
|
||||
type ProxyClassReconciler struct {
|
||||
client.Client
|
||||
|
||||
recorder record.EventRecorder
|
||||
logger *zap.SugaredLogger
|
||||
clock tstime.Clock
|
||||
}
|
||||
|
||||
func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
|
||||
logger := pcr.logger.With("ProxyClass", req.Name)
|
||||
logger.Debugf("starting reconcile")
|
||||
defer logger.Debugf("reconcile finished")
|
||||
|
||||
pc := new(tsapi.ProxyClass)
|
||||
err = pcr.Get(ctx, req.NamespacedName, pc)
|
||||
if apierrors.IsNotFound(err) {
|
||||
logger.Debugf("ProxyClass not found, assuming it was deleted")
|
||||
return reconcile.Result{}, nil
|
||||
} else if err != nil {
|
||||
return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com ProxyClass: %w", err)
|
||||
}
|
||||
if !pc.DeletionTimestamp.IsZero() {
|
||||
logger.Debugf("ProxyClass is being deleted, do nothing")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
oldPCStatus := pc.Status.DeepCopy()
|
||||
if errs := pcr.validate(pc); errs != nil {
|
||||
msg := fmt.Sprintf(messageProxyClassInvalid, errs.ToAggregate().Error())
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonProxyClassInvalid, msg)
|
||||
tsoperator.SetProxyClassCondition(pc, tsapi.ProxyClassready, metav1.ConditionFalse, reasonProxyClassInvalid, msg, pc.Generation, pcr.clock, logger)
|
||||
} else {
|
||||
tsoperator.SetProxyClassCondition(pc, tsapi.ProxyClassready, metav1.ConditionTrue, reasonProxyClassValid, reasonProxyClassValid, pc.Generation, pcr.clock, logger)
|
||||
}
|
||||
if !apiequality.Semantic.DeepEqual(oldPCStatus, pc.Status) {
|
||||
if err := pcr.Client.Status().Update(ctx, pc); err != nil {
|
||||
logger.Errorf("error updating ProxyClass status: %v", err)
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.ErrorList) {
|
||||
if sts := pc.Spec.StatefulSet; sts != nil {
|
||||
if len(sts.Labels) > 0 {
|
||||
if errs := metavalidation.ValidateLabels(sts.Labels, field.NewPath(".spec.statefulSet.labels")); errs != nil {
|
||||
violations = append(violations, errs...)
|
||||
}
|
||||
}
|
||||
if len(sts.Annotations) > 0 {
|
||||
if errs := apivalidation.ValidateAnnotations(sts.Annotations, field.NewPath(".spec.statefulSet.annotations")); errs != nil {
|
||||
violations = append(violations, errs...)
|
||||
}
|
||||
}
|
||||
if pod := sts.Pod; pod != nil {
|
||||
if len(pod.Labels) > 0 {
|
||||
if errs := metavalidation.ValidateLabels(pod.Labels, field.NewPath(".spec.statefulSet.pod.labels")); errs != nil {
|
||||
violations = append(violations, errs...)
|
||||
}
|
||||
}
|
||||
if len(pod.Annotations) > 0 {
|
||||
if errs := apivalidation.ValidateAnnotations(pod.Annotations, field.NewPath(".spec.statefulSet.pod.annotations")); errs != nil {
|
||||
violations = append(violations, errs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We do not validate embedded fields (security context, resource
|
||||
// requirements etc) as we inherit upstream validation for those fields.
|
||||
// Invalid values would get rejected by upstream validations at apply
|
||||
// time.
|
||||
return violations
|
||||
}
|
||||
83
cmd/k8s-operator/proxyclass_test.go
Normal file
83
cmd/k8s-operator/proxyclass_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
||||
// cluster to your Tailnet.
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestProxyClass(t *testing.T) {
|
||||
pc := &tsapi.ProxyClass{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "ProxyClass", APIVersion: "tailscale.com/v1alpha1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
},
|
||||
Spec: tsapi.ProxyClassSpec{
|
||||
StatefulSet: &tsapi.StatefulSet{
|
||||
Labels: map[string]string{"foo": "bar", "xyz1234": "abc567"},
|
||||
Annotations: map[string]string{"foo.io/bar": "{'key': 'val1232'}"},
|
||||
Pod: &tsapi.Pod{
|
||||
Labels: map[string]string{"foo": "bar", "xyz1234": "abc567"},
|
||||
Annotations: map[string]string{"foo.io/bar": "{'key': 'val1232'}"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fc := fake.NewClientBuilder().
|
||||
WithScheme(tsapi.GlobalScheme).
|
||||
WithObjects(pc).
|
||||
WithStatusSubresource(pc).
|
||||
Build()
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||
pcr := &ProxyClassReconciler{
|
||||
Client: fc,
|
||||
logger: zl.Sugar(),
|
||||
clock: cl,
|
||||
recorder: record.NewFakeRecorder(1),
|
||||
}
|
||||
expectReconciled(t, pcr, "", "test")
|
||||
|
||||
// 1. A valid ProxyClass resource gets its status updated to Ready.
|
||||
pc.Status.Conditions = append(pc.Status.Conditions, tsapi.ConnectorCondition{
|
||||
Type: tsapi.ProxyClassready,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonProxyClassValid,
|
||||
Message: reasonProxyClassValid,
|
||||
LastTransitionTime: &metav1.Time{Time: cl.Now().Truncate(time.Second)},
|
||||
})
|
||||
|
||||
expectEqual(t, fc, pc)
|
||||
|
||||
// 2. An invalid ProxyClass resource gets its status updated to Invalid.
|
||||
pc.Spec.StatefulSet.Labels["foo"] = "?!someVal"
|
||||
mustUpdate(t, fc, "", "test", func(proxyClass *tsapi.ProxyClass) {
|
||||
proxyClass.Spec.StatefulSet.Labels = pc.Spec.StatefulSet.Labels
|
||||
})
|
||||
expectReconciled(t, pcr, "", "test")
|
||||
msg := `ProxyClass is not valid: .spec.statefulSet.labels: Invalid value: "?!someVal": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`
|
||||
tsoperator.SetProxyClassCondition(pc, tsapi.ProxyClassready, metav1.ConditionFalse, reasonProxyClassInvalid, msg, 0, cl, zl.Sugar())
|
||||
expectEqual(t, fc, pc)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -22,25 +23,36 @@ import (
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
const (
|
||||
// Labels that the operator sets on StatefulSets and Pods. If you add a
|
||||
// new label here, do also add it to tailscaleManagedLabels var to
|
||||
// ensure that it does not get overwritten by ProxyClass configuration.
|
||||
LabelManaged = "tailscale.com/managed"
|
||||
LabelParentType = "tailscale.com/parent-resource-type"
|
||||
LabelParentName = "tailscale.com/parent-resource"
|
||||
LabelParentNamespace = "tailscale.com/parent-resource-ns"
|
||||
|
||||
// LabelProxyClass can be set by users on Connectors, tailscale
|
||||
// Ingresses and Services that define cluster ingress or cluster egress,
|
||||
// to specify that configuration in this ProxyClass should be applied to
|
||||
// resources created for the Connector, Ingress or Service.
|
||||
LabelProxyClass = "tailscale.com/proxy-class"
|
||||
|
||||
FinalizerName = "tailscale.com/finalizer"
|
||||
|
||||
// Annotations settable by users on services.
|
||||
@@ -55,8 +67,27 @@ const (
|
||||
// Annotations settable by users on ingresses.
|
||||
AnnotationFunnel = "tailscale.com/funnel"
|
||||
|
||||
// Tailnet VIP that this proxy should satisfy
|
||||
AnnotationTSVIP = "tailscale.com/expose-via-vip"
|
||||
|
||||
// If set to true, set up iptables/nftables rules in the proxy forward
|
||||
// cluster traffic to the tailnet IP of that proxy. This can only be set
|
||||
// on an Ingress. This is useful in cases where a cluster target needs
|
||||
// to be able to reach a cluster workload exposed to tailnet via Ingress
|
||||
// using the same hostname as a tailnet workload (in this case, the
|
||||
// MagicDNS name of the ingress proxy). This annotation is experimental.
|
||||
// If it is set to true, the proxy set up for Ingress, will run
|
||||
// tailscale in non-userspace, with NET_ADMIN cap for tailscale
|
||||
// container and will also run a privileged init container that enables
|
||||
// forwarding.
|
||||
// Eventually this behaviour might become the default.
|
||||
AnnotationExperimentalForwardClusterTrafficViaL7IngresProxy = "tailscale.com/experimental-forward-cluster-traffic-via-ingress"
|
||||
|
||||
// Annotations set by the operator on pods to trigger restarts when the
|
||||
// hostname, IP, FQDN or tailscaled config changes.
|
||||
// hostname, IP, FQDN or tailscaled config changes. If you add a new
|
||||
// annotation here, also add it to tailscaleManagedAnnotations var to
|
||||
// ensure that it does not get removed when a ProxyClass configuration
|
||||
// is applied.
|
||||
podAnnotationLastSetClusterIP = "tailscale.com/operator-last-set-cluster-ip"
|
||||
podAnnotationLastSetHostname = "tailscale.com/operator-last-set-hostname"
|
||||
podAnnotationLastSetTailnetTargetIP = "tailscale.com/operator-last-set-ts-tailnet-target-ip"
|
||||
@@ -69,13 +100,23 @@ const (
|
||||
tailscaledConfigKey = "tailscaled"
|
||||
)
|
||||
|
||||
var (
|
||||
// tailscaleManagedLabels are label keys that tailscale operator sets on StatefulSets and Pods.
|
||||
tailscaleManagedLabels = []string{LabelManaged, LabelParentType, LabelParentName, LabelParentNamespace, "app"}
|
||||
// tailscaleManagedAnnotations are annotation keys that tailscale operator sets on StatefulSets and Pods.
|
||||
tailscaleManagedAnnotations = []string{podAnnotationLastSetClusterIP, podAnnotationLastSetHostname, podAnnotationLastSetTailnetTargetIP, podAnnotationLastSetTailnetTargetFQDN, podAnnotationLastSetConfigFileHash}
|
||||
)
|
||||
|
||||
type tailscaleSTSConfig struct {
|
||||
ParentResourceName string
|
||||
ParentResourceUID string
|
||||
ChildResourceLabels map[string]string
|
||||
|
||||
ServeConfig *ipn.ServeConfig
|
||||
ClusterTargetIP string // ingress target
|
||||
ServeConfig *ipn.ServeConfig // if serve config is set, this is a proxy for Ingress
|
||||
ClusterTargetIP string // ingress target
|
||||
// If set to true, operator should configure containerboot to forward
|
||||
// cluster traffic via the proxy set up for Kubernetes Ingress.
|
||||
ForwardClusterTrafficViaL7IngressProxy bool
|
||||
|
||||
TailnetTargetIP string // egress target IP
|
||||
|
||||
@@ -87,6 +128,9 @@ type tailscaleSTSConfig struct {
|
||||
// Connector specifies a configuration of a Connector instance if that's
|
||||
// what this StatefulSet should be created for.
|
||||
Connector *connector
|
||||
|
||||
ProxyClass string
|
||||
TSVIP string // a tailnet VIP that should route to this proxy
|
||||
}
|
||||
|
||||
type connector struct {
|
||||
@@ -95,10 +139,13 @@ type connector struct {
|
||||
// isExitNode defines whether this Connector should act as an exit node.
|
||||
isExitNode bool
|
||||
}
|
||||
type tsnetServer interface {
|
||||
CertDomains() []string
|
||||
}
|
||||
|
||||
type tailscaleSTSReconciler struct {
|
||||
client.Client
|
||||
tsnetServer *tsnet.Server
|
||||
tsnetServer tsnetServer
|
||||
tsClient tsClient
|
||||
defaultTags []string
|
||||
operatorNamespace string
|
||||
@@ -265,9 +312,7 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var (
|
||||
authKey, hash string
|
||||
)
|
||||
var hash string
|
||||
if orig == nil {
|
||||
// Secret doesn't exist yet, create one. Initially it contains
|
||||
// only the Tailscale authkey, but once Tailscale starts it'll
|
||||
@@ -284,21 +329,23 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
||||
}
|
||||
// Create API Key secret which is going to be used by the statefulset
|
||||
// to authenticate with Tailscale.
|
||||
logger.Debugf("creating authkey for new tailscale proxy")
|
||||
tags := stsC.Tags
|
||||
if len(tags) == 0 {
|
||||
tags = a.defaultTags
|
||||
}
|
||||
authKey, err = a.newAuthKey(ctx, tags)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
if !shouldDoTailscaledDeclarativeConfig(stsC) && authKey != "" {
|
||||
mak.Set(&secret.StringData, "authkey", authKey)
|
||||
// logger.Debugf("creating authkey for new tailscale proxy")
|
||||
// tags := stsC.Tags
|
||||
// if len(tags) == 0 {
|
||||
// tags = a.defaultTags
|
||||
// }
|
||||
// authKey, err = a.newAuthKey(ctx, tags)
|
||||
// if err != nil {
|
||||
// return "", "", err
|
||||
// }
|
||||
}
|
||||
// if !shouldDoTailscaledDeclarativeConfig(stsC) && authKey != "" {
|
||||
// mak.Set(&secret.StringData, "authkey", authKey)
|
||||
// }
|
||||
|
||||
// TODO: this is going to be broken now, fix
|
||||
if shouldDoTailscaledDeclarativeConfig(stsC) {
|
||||
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig)
|
||||
confFileBytes, h, err := tailscaledConfig(stsC, "", orig)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error creating tailscaled config: %w", err)
|
||||
}
|
||||
@@ -380,10 +427,10 @@ var proxyYaml []byte
|
||||
var userspaceProxyYaml []byte
|
||||
|
||||
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, proxySecret, tsConfigHash string) (*appsv1.StatefulSet, error) {
|
||||
var ss appsv1.StatefulSet
|
||||
if sts.ServeConfig != nil {
|
||||
ss := new(appsv1.StatefulSet)
|
||||
if sts.ServeConfig != nil && sts.ForwardClusterTrafficViaL7IngressProxy != true && sts.TSVIP == "" { // If forwarding cluster traffic via is required we need non-userspace + NET_ADMIN + forwarding
|
||||
if err := yaml.Unmarshal(userspaceProxyYaml, &ss); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal proxy spec: %w", err)
|
||||
return nil, fmt.Errorf("failed to unmarshal userspace proxy spec: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := yaml.Unmarshal(proxyYaml, &ss); err != nil {
|
||||
@@ -397,12 +444,25 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
}
|
||||
}
|
||||
}
|
||||
container := &ss.Spec.Template.Spec.Containers[0]
|
||||
pod := &ss.Spec.Template
|
||||
container := &pod.Spec.Containers[0]
|
||||
proxyClass := new(tsapi.ProxyClass)
|
||||
if sts.ProxyClass != "" {
|
||||
if err := a.Get(ctx, types.NamespacedName{Name: sts.ProxyClass}, proxyClass); err != nil {
|
||||
return nil, fmt.Errorf("failed to get ProxyClass: %w", err)
|
||||
}
|
||||
if !tsoperator.ProxyClassIsReady(proxyClass) {
|
||||
logger.Infof("ProxyClass %s specified for the proxy, but it is not (yet) in a ready state, waiting..")
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
container.Image = a.proxyImage
|
||||
ss.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: headlessSvc.Name,
|
||||
Namespace: a.operatorNamespace,
|
||||
Labels: sts.ChildResourceLabels,
|
||||
}
|
||||
for key, val := range sts.ChildResourceLabels {
|
||||
mak.Set(&ss.ObjectMeta.Labels, key, val)
|
||||
}
|
||||
ss.Spec.ServiceName = headlessSvc.Name
|
||||
ss.Spec.Selector = &metav1.LabelSelector{
|
||||
@@ -410,9 +470,9 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
"app": sts.ParentResourceUID,
|
||||
},
|
||||
}
|
||||
mak.Set(&ss.Spec.Template.Labels, "app", sts.ParentResourceUID)
|
||||
mak.Set(&pod.Labels, "app", sts.ParentResourceUID)
|
||||
for key, val := range sts.ChildResourceLabels {
|
||||
ss.Spec.Template.Labels[key] = val // sync StatefulSet labels to Pod to make it easier for users to select the Pod
|
||||
pod.Labels[key] = val // sync StatefulSet labels to Pod to make it easier for users to select the Pod
|
||||
}
|
||||
|
||||
// Generic containerboot configuration options.
|
||||
@@ -422,6 +482,23 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
Value: proxySecret,
|
||||
},
|
||||
)
|
||||
if sts.ForwardClusterTrafficViaL7IngressProxy {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS",
|
||||
Value: "true",
|
||||
})
|
||||
}
|
||||
if sts.TSVIP != "" {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: "EXPERIMENTAL_TS_VIP",
|
||||
Value: sts.TSVIP + "/32",
|
||||
})
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: "TS_ROUTES",
|
||||
Value: sts.TSVIP + "/32",
|
||||
})
|
||||
|
||||
}
|
||||
if !shouldDoTailscaledDeclarativeConfig(sts) {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: "TS_HOSTNAME",
|
||||
@@ -431,12 +508,12 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
// it is passed via an environment variable. So we need to restart the
|
||||
// container when the value changes. We do this by adding an annotation to
|
||||
// the pod template that contains the last value we set.
|
||||
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetHostname, sts.Hostname)
|
||||
mak.Set(&pod.Annotations, podAnnotationLastSetHostname, sts.Hostname)
|
||||
}
|
||||
// Configure containeboot to run tailscaled with a configfile read from the state Secret.
|
||||
if shouldDoTailscaledDeclarativeConfig(sts) {
|
||||
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash, tsConfigHash)
|
||||
ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: "tailscaledconfig",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
@@ -465,7 +542,13 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
Value: a.tsFirewallMode,
|
||||
})
|
||||
}
|
||||
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
|
||||
pod.Spec.PriorityClassName = a.proxyPriorityClassName
|
||||
|
||||
keyURL := fmt.Sprintf("http://keyserver.%s.svc.cluster.local:8443/keys", a.operatorNamespace)
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
Name: "EXPERIMENTAL_AUTH_KEYS_ENDPOINT",
|
||||
Value: keyURL,
|
||||
})
|
||||
|
||||
// Ingress/egress proxy configuration options.
|
||||
if sts.ClusterTargetIP != "" {
|
||||
@@ -496,7 +579,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
ReadOnly: true,
|
||||
MountPath: "/etc/tailscaled",
|
||||
})
|
||||
ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
Name: "serve-config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
@@ -510,7 +593,99 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
})
|
||||
}
|
||||
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
|
||||
if sts.ProxyClass != "" {
|
||||
logger.Debugf("configuring proxy resources with ProxyClass %s", sts.ProxyClass)
|
||||
ss = applyProxyClassToStatefulSet(proxyClass, ss)
|
||||
}
|
||||
updateSS := func(s *appsv1.StatefulSet) {
|
||||
s.Spec = ss.Spec
|
||||
s.ObjectMeta.Labels = ss.Labels
|
||||
s.ObjectMeta.Annotations = ss.Annotations
|
||||
}
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, ss, updateSS)
|
||||
}
|
||||
|
||||
// mergeStatefulSetLabelsOrAnnots returns a map that contains all keys/values
|
||||
// present in 'custom' map as well as those keys/values from the current map
|
||||
// whose keys are present in the 'managed' map. The reason why this merge is
|
||||
// necessary is to ensure that labels/annotations applied from a ProxyClass get removed
|
||||
// if they are removed from a ProxyClass or if the ProxyClass no longer applies
|
||||
// to this StatefulSet whilst any tailscale managed labels/annotations remain present.
|
||||
func mergeStatefulSetLabelsOrAnnots(current, custom map[string]string, managed []string) map[string]string {
|
||||
if custom == nil {
|
||||
custom = make(map[string]string)
|
||||
}
|
||||
if current == nil {
|
||||
return custom
|
||||
}
|
||||
for key, val := range current {
|
||||
if slices.Contains(managed, key) {
|
||||
custom[key] = val
|
||||
}
|
||||
}
|
||||
return custom
|
||||
}
|
||||
|
||||
func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet) *appsv1.StatefulSet {
|
||||
if pc == nil || ss == nil || pc.Spec.StatefulSet == nil {
|
||||
return ss
|
||||
}
|
||||
|
||||
// Update StatefulSet metadata.
|
||||
if wantsSSLabels := pc.Spec.StatefulSet.Labels; len(wantsSSLabels) > 0 {
|
||||
ss.ObjectMeta.Labels = mergeStatefulSetLabelsOrAnnots(ss.ObjectMeta.Labels, wantsSSLabels, tailscaleManagedLabels)
|
||||
}
|
||||
if wantsSSAnnots := pc.Spec.StatefulSet.Annotations; len(wantsSSAnnots) > 0 {
|
||||
ss.ObjectMeta.Annotations = mergeStatefulSetLabelsOrAnnots(ss.ObjectMeta.Annotations, wantsSSAnnots, tailscaleManagedAnnotations)
|
||||
}
|
||||
|
||||
if pc.Spec.StatefulSet.Replicas != nil {
|
||||
ss.Spec.Replicas = pc.Spec.StatefulSet.Replicas
|
||||
}
|
||||
|
||||
// Update Pod fields.
|
||||
if pc.Spec.StatefulSet.Pod == nil {
|
||||
return ss
|
||||
}
|
||||
wantsPod := pc.Spec.StatefulSet.Pod
|
||||
if wantsPodLabels := wantsPod.Labels; len(wantsPodLabels) > 0 {
|
||||
ss.Spec.Template.ObjectMeta.Labels = mergeStatefulSetLabelsOrAnnots(ss.Spec.Template.ObjectMeta.Labels, wantsPodLabels, tailscaleManagedLabels)
|
||||
}
|
||||
if wantsPodAnnots := wantsPod.Annotations; len(wantsPodAnnots) > 0 {
|
||||
ss.Spec.Template.ObjectMeta.Annotations = mergeStatefulSetLabelsOrAnnots(ss.Spec.Template.ObjectMeta.Annotations, wantsPodAnnots, tailscaleManagedAnnotations)
|
||||
}
|
||||
ss.Spec.Template.Spec.SecurityContext = wantsPod.SecurityContext
|
||||
ss.Spec.Template.Spec.ImagePullSecrets = wantsPod.ImagePullSecrets
|
||||
ss.Spec.Template.Spec.NodeName = wantsPod.NodeName
|
||||
ss.Spec.Template.Spec.NodeSelector = wantsPod.NodeSelector
|
||||
ss.Spec.Template.Spec.Tolerations = wantsPod.Tolerations
|
||||
|
||||
// Update containers.
|
||||
updateContainer := func(overlay *tsapi.Container, base corev1.Container) corev1.Container {
|
||||
if overlay == nil {
|
||||
return base
|
||||
}
|
||||
if overlay.SecurityContext != nil {
|
||||
base.SecurityContext = overlay.SecurityContext
|
||||
}
|
||||
base.Resources = overlay.Resources
|
||||
return base
|
||||
}
|
||||
for i, c := range ss.Spec.Template.Spec.Containers {
|
||||
if c.Name == "tailscale" {
|
||||
ss.Spec.Template.Spec.Containers[i] = updateContainer(wantsPod.TailscaleContainer, ss.Spec.Template.Spec.Containers[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
if initContainers := ss.Spec.Template.Spec.InitContainers; len(initContainers) > 0 {
|
||||
for i, c := range initContainers {
|
||||
if c.Name == "sysctler" {
|
||||
ss.Spec.Template.Spec.InitContainers[i] = updateContainer(wantsPod.TailscaleInitContainer, initContainers[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// tailscaledConfig takes a proxy config, a newly generated auth key if
|
||||
|
||||
@@ -6,10 +6,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"sigs.k8s.io/yaml"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Test_statefulSetNameBase tests that parent name portion in a StatefulSet name
|
||||
@@ -39,3 +49,245 @@ func Test_statefulSetNameBase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_applyProxyClassToStatefulSet(t *testing.T) {
|
||||
// Setup
|
||||
proxyClassAllOpts := &tsapi.ProxyClass{
|
||||
Spec: tsapi.ProxyClassSpec{
|
||||
StatefulSet: &tsapi.StatefulSet{
|
||||
Labels: map[string]string{"foo": "bar"},
|
||||
Annotations: map[string]string{"foo.io/bar": "foo"},
|
||||
Pod: &tsapi.Pod{
|
||||
Labels: map[string]string{"bar": "foo"},
|
||||
Annotations: map[string]string{"bar.io/foo": "foo"},
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsUser: ptr.To(int64(0)),
|
||||
},
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{{Name: "docker-creds"}},
|
||||
NodeName: "some-node",
|
||||
NodeSelector: map[string]string{"beta.kubernetes.io/os": "linux"},
|
||||
Tolerations: []corev1.Toleration{{Key: "", Operator: "Exists"}},
|
||||
TailscaleContainer: &tsapi.Container{
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
},
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1000m"), corev1.ResourceMemory: resource.MustParse("128Mi")},
|
||||
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("64Mi")},
|
||||
},
|
||||
},
|
||||
TailscaleInitContainer: &tsapi.Container{
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
RunAsUser: ptr.To(int64(0)),
|
||||
},
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1000m"), corev1.ResourceMemory: resource.MustParse("128Mi")},
|
||||
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("64Mi")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
proxyClassJustLabels := &tsapi.ProxyClass{
|
||||
Spec: tsapi.ProxyClassSpec{
|
||||
StatefulSet: &tsapi.StatefulSet{
|
||||
Labels: map[string]string{"foo": "bar"},
|
||||
Annotations: map[string]string{"foo.io/bar": "foo"},
|
||||
Pod: &tsapi.Pod{
|
||||
Labels: map[string]string{"bar": "foo"},
|
||||
Annotations: map[string]string{"bar.io/foo": "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var userspaceProxySS, nonUserspaceProxySS appsv1.StatefulSet
|
||||
if err := yaml.Unmarshal(userspaceProxyYaml, &userspaceProxySS); err != nil {
|
||||
t.Fatalf("unmarshaling userspace proxy template: %v", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(proxyYaml, &nonUserspaceProxySS); err != nil {
|
||||
t.Fatalf("unmarshaling non-userspace proxy template: %v", err)
|
||||
}
|
||||
// Set a couple additional fields so we can test that we don't
|
||||
// mistakenly override those.
|
||||
labels := map[string]string{
|
||||
LabelManaged: "true",
|
||||
LabelParentName: "foo",
|
||||
}
|
||||
annots := map[string]string{
|
||||
podAnnotationLastSetClusterIP: "1.2.3.4",
|
||||
}
|
||||
env := []corev1.EnvVar{{Name: "TS_HOSTNAME", Value: "nginx"}}
|
||||
userspaceProxySS.Labels = labels
|
||||
userspaceProxySS.Annotations = annots
|
||||
userspaceProxySS.Spec.Template.Spec.Containers[0].Env = env
|
||||
nonUserspaceProxySS.ObjectMeta.Labels = labels
|
||||
nonUserspaceProxySS.ObjectMeta.Annotations = annots
|
||||
nonUserspaceProxySS.Spec.Template.Spec.Containers[0].Env = env
|
||||
|
||||
// 1. Test that a ProxyClass with all fields set gets correctly applied
|
||||
// to a Statefulset built from non-userspace proxy template.
|
||||
wantSS := nonUserspaceProxySS.DeepCopy()
|
||||
wantSS.ObjectMeta.Labels = mergeMapKeys(wantSS.ObjectMeta.Labels, proxyClassAllOpts.Spec.StatefulSet.Labels)
|
||||
wantSS.ObjectMeta.Annotations = mergeMapKeys(wantSS.ObjectMeta.Annotations, proxyClassAllOpts.Spec.StatefulSet.Annotations)
|
||||
wantSS.Spec.Template.Labels = proxyClassAllOpts.Spec.StatefulSet.Pod.Labels
|
||||
wantSS.Spec.Template.Annotations = proxyClassAllOpts.Spec.StatefulSet.Pod.Annotations
|
||||
wantSS.Spec.Template.Spec.SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.SecurityContext
|
||||
wantSS.Spec.Template.Spec.ImagePullSecrets = proxyClassAllOpts.Spec.StatefulSet.Pod.ImagePullSecrets
|
||||
wantSS.Spec.Template.Spec.NodeName = proxyClassAllOpts.Spec.StatefulSet.Pod.NodeName
|
||||
wantSS.Spec.Template.Spec.NodeSelector = proxyClassAllOpts.Spec.StatefulSet.Pod.NodeSelector
|
||||
wantSS.Spec.Template.Spec.Tolerations = proxyClassAllOpts.Spec.StatefulSet.Pod.Tolerations
|
||||
wantSS.Spec.Template.Spec.Containers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.SecurityContext
|
||||
wantSS.Spec.Template.Spec.InitContainers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleInitContainer.SecurityContext
|
||||
wantSS.Spec.Template.Spec.Containers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.Resources
|
||||
wantSS.Spec.Template.Spec.InitContainers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleInitContainer.Resources
|
||||
|
||||
gotSS := applyProxyClassToStatefulSet(proxyClassAllOpts, nonUserspaceProxySS.DeepCopy())
|
||||
if diff := cmp.Diff(gotSS, wantSS); diff != "" {
|
||||
t.Fatalf("Unexpected result applying ProxyClass with all fields set to a StatefulSet for non-userspace proxy (-got +want):\n%s", diff)
|
||||
}
|
||||
|
||||
// 2. Test that a ProxyClass with custom labels and annotations for
|
||||
// StatefulSet and Pod set gets correctly applied to a Statefulset built
|
||||
// from non-userspace proxy template.
|
||||
wantSS = nonUserspaceProxySS.DeepCopy()
|
||||
wantSS.ObjectMeta.Labels = mergeMapKeys(wantSS.ObjectMeta.Labels, proxyClassJustLabels.Spec.StatefulSet.Labels)
|
||||
wantSS.ObjectMeta.Annotations = mergeMapKeys(wantSS.ObjectMeta.Annotations, proxyClassJustLabels.Spec.StatefulSet.Annotations)
|
||||
wantSS.Spec.Template.Labels = proxyClassJustLabels.Spec.StatefulSet.Pod.Labels
|
||||
wantSS.Spec.Template.Annotations = proxyClassJustLabels.Spec.StatefulSet.Pod.Annotations
|
||||
gotSS = applyProxyClassToStatefulSet(proxyClassJustLabels, nonUserspaceProxySS.DeepCopy())
|
||||
if diff := cmp.Diff(gotSS, wantSS); diff != "" {
|
||||
t.Fatalf("Unexpected result applying ProxyClass with custom labels and annotations to a StatefulSet for non-userspace proxy (-got +want):\n%s", diff)
|
||||
}
|
||||
|
||||
// 3. Test that a ProxyClass with all fields set gets correctly applied
|
||||
// to a Statefulset built from a userspace proxy template.
|
||||
wantSS = userspaceProxySS.DeepCopy()
|
||||
wantSS.ObjectMeta.Labels = mergeMapKeys(wantSS.ObjectMeta.Labels, proxyClassAllOpts.Spec.StatefulSet.Labels)
|
||||
wantSS.ObjectMeta.Annotations = mergeMapKeys(wantSS.ObjectMeta.Annotations, proxyClassAllOpts.Spec.StatefulSet.Annotations)
|
||||
wantSS.Spec.Template.Labels = proxyClassAllOpts.Spec.StatefulSet.Pod.Labels
|
||||
wantSS.Spec.Template.Annotations = proxyClassAllOpts.Spec.StatefulSet.Pod.Annotations
|
||||
wantSS.Spec.Template.Spec.SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.SecurityContext
|
||||
wantSS.Spec.Template.Spec.ImagePullSecrets = proxyClassAllOpts.Spec.StatefulSet.Pod.ImagePullSecrets
|
||||
wantSS.Spec.Template.Spec.NodeName = proxyClassAllOpts.Spec.StatefulSet.Pod.NodeName
|
||||
wantSS.Spec.Template.Spec.NodeSelector = proxyClassAllOpts.Spec.StatefulSet.Pod.NodeSelector
|
||||
wantSS.Spec.Template.Spec.Tolerations = proxyClassAllOpts.Spec.StatefulSet.Pod.Tolerations
|
||||
wantSS.Spec.Template.Spec.Containers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.SecurityContext
|
||||
wantSS.Spec.Template.Spec.Containers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.Resources
|
||||
gotSS = applyProxyClassToStatefulSet(proxyClassAllOpts, userspaceProxySS.DeepCopy())
|
||||
if diff := cmp.Diff(gotSS, wantSS); diff != "" {
|
||||
t.Fatalf("Unexpected result applying ProxyClass with custom labels and annotations to a StatefulSet for a userspace proxy (-got +want):\n%s", diff)
|
||||
}
|
||||
|
||||
// 4. Test that a ProxyClass with custom labels and annotations gets correctly applied
|
||||
// to a Statefulset built from a userspace proxy template.
|
||||
wantSS = userspaceProxySS.DeepCopy()
|
||||
wantSS.ObjectMeta.Labels = mergeMapKeys(wantSS.ObjectMeta.Labels, proxyClassJustLabels.Spec.StatefulSet.Labels)
|
||||
wantSS.ObjectMeta.Annotations = mergeMapKeys(wantSS.ObjectMeta.Annotations, proxyClassJustLabels.Spec.StatefulSet.Annotations)
|
||||
wantSS.Spec.Template.Labels = proxyClassJustLabels.Spec.StatefulSet.Pod.Labels
|
||||
wantSS.Spec.Template.Annotations = proxyClassJustLabels.Spec.StatefulSet.Pod.Annotations
|
||||
gotSS = applyProxyClassToStatefulSet(proxyClassJustLabels, userspaceProxySS.DeepCopy())
|
||||
if diff := cmp.Diff(gotSS, wantSS); diff != "" {
|
||||
t.Fatalf("Unexpected result applying ProxyClass with custom labels and annotations to a StatefulSet for a userspace proxy (-got +want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMapKeys(a, b map[string]string) map[string]string {
|
||||
for key, val := range b {
|
||||
a[key] = val
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func Test_mergeStatefulSetLabelsOrAnnots(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current map[string]string
|
||||
custom map[string]string
|
||||
managed []string
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
name: "no custom labels specified and none present in current labels, return current labels",
|
||||
current: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
want: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
managed: tailscaleManagedLabels,
|
||||
},
|
||||
{
|
||||
name: "no custom labels specified, but some present in current labels, return tailscale managed labels only from the current labels",
|
||||
current: map[string]string{"foo": "bar", "something.io/foo": "bar", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
want: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
managed: tailscaleManagedLabels,
|
||||
},
|
||||
{
|
||||
name: "custom labels specified, current labels only contain tailscale managed labels, return a union of both",
|
||||
current: map[string]string{LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
managed: tailscaleManagedLabels,
|
||||
},
|
||||
{
|
||||
name: "custom labels specified, current labels contain tailscale managed labels and custom labels, some of which re not present in the new custom labels, return a union of managed labels and the desired custom labels",
|
||||
current: map[string]string{"foo": "bar", "bar": "baz", "app": "1234", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar", "app": "1234", LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
|
||||
managed: tailscaleManagedLabels,
|
||||
},
|
||||
{
|
||||
name: "no current labels present, return custom labels only",
|
||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
managed: tailscaleManagedLabels,
|
||||
},
|
||||
{
|
||||
name: "no current labels present, no custom labels specified, return empty map",
|
||||
want: map[string]string{},
|
||||
managed: tailscaleManagedLabels,
|
||||
},
|
||||
{
|
||||
name: "no custom annots specified and none present in current annots, return current annots",
|
||||
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
managed: tailscaleManagedAnnotations,
|
||||
},
|
||||
{
|
||||
name: "no custom annots specified, but some present in current annots, return tailscale managed annots only from the current annots",
|
||||
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
managed: tailscaleManagedAnnotations,
|
||||
},
|
||||
{
|
||||
name: "custom annots specified, current annots only contain tailscale managed annots, return a union of both",
|
||||
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
managed: tailscaleManagedAnnotations,
|
||||
},
|
||||
{
|
||||
name: "custom annots specified, current annots contain tailscale managed annots and custom annots, some of which are not present in the new custom annots, return a union of managed annots and the desired custom annots",
|
||||
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
custom: map[string]string{"something.io/foo": "bar"},
|
||||
want: map[string]string{"something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
|
||||
managed: tailscaleManagedAnnotations,
|
||||
},
|
||||
{
|
||||
name: "no current annots present, return custom annots only",
|
||||
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
want: map[string]string{"foo": "bar", "something.io/foo": "bar"},
|
||||
managed: tailscaleManagedAnnotations,
|
||||
},
|
||||
{
|
||||
name: "no current labels present, no custom labels specified, return empty map",
|
||||
want: map[string]string{},
|
||||
managed: tailscaleManagedAnnotations,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := mergeStatefulSetLabelsOrAnnots(tt.current, tt.custom, tt.managed); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mergeStatefulSetLabels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
@@ -155,6 +157,17 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
a.logger.Error(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
proxyClass := proxyClassForObject(svc)
|
||||
if proxyClass != "" {
|
||||
if ready, err := proxyClassIsReady(ctx, proxyClass, a.Client); err != nil {
|
||||
return fmt.Errorf("error verifying ProxyClass for Service: %w", err)
|
||||
} else if !ready {
|
||||
logger.Infof("ProxyClass %s specified for the Service, but is not (yet) Ready, waiting..", proxyClass)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
hostname, err := nameForService(svc)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -183,6 +196,8 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
Hostname: hostname,
|
||||
Tags: tags,
|
||||
ChildResourceLabels: crl,
|
||||
ProxyClass: proxyClass,
|
||||
TSVIP: a.tailnetVIPForService(svc),
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
@@ -318,3 +333,22 @@ func (a *ServiceReconciler) tailnetTargetAnnotation(svc *corev1.Service) string
|
||||
}
|
||||
return svc.Annotations[annotationTailnetTargetIPOld]
|
||||
}
|
||||
|
||||
func proxyClassForObject(o client.Object) string {
|
||||
return o.GetLabels()[LabelProxyClass]
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) tailnetVIPForService(svc *corev1.Service) string {
|
||||
if !a.shouldExpose(svc) || svc.Annotations == nil {
|
||||
return ""
|
||||
}
|
||||
return svc.GetAnnotations()[AnnotationTSVIP]
|
||||
}
|
||||
|
||||
func proxyClassIsReady(ctx context.Context, name string, cl client.Client) (bool, error) {
|
||||
proxyClass := new(tsapi.ProxyClass)
|
||||
if err := cl.Get(ctx, types.NamespacedName{Name: name}, proxyClass); err != nil {
|
||||
return false, fmt.Errorf("error getting ProxyClass %s: %w", name, err)
|
||||
}
|
||||
return tsoperator.ProxyClassIsReady(proxyClass), nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
@@ -31,29 +32,34 @@ import (
|
||||
// confgOpts contains configuration options for creating cluster resources for
|
||||
// Tailscale proxies.
|
||||
type configOpts struct {
|
||||
stsName string
|
||||
secretName string
|
||||
hostname string
|
||||
namespace string
|
||||
parentType string
|
||||
priorityClassName string
|
||||
firewallMode string
|
||||
tailnetTargetIP string
|
||||
tailnetTargetFQDN string
|
||||
clusterTargetIP string
|
||||
subnetRoutes string
|
||||
isExitNode bool
|
||||
shouldUseDeclarativeConfig bool // tailscaled in proxy should be configured using config file
|
||||
confFileHash string
|
||||
stsName string
|
||||
secretName string
|
||||
hostname string
|
||||
namespace string
|
||||
parentType string
|
||||
priorityClassName string
|
||||
firewallMode string
|
||||
tailnetTargetIP string
|
||||
tailnetTargetFQDN string
|
||||
clusterTargetIP string
|
||||
subnetRoutes string
|
||||
isExitNode bool
|
||||
shouldUseDeclarativeConfig bool // tailscaled in proxy should be configured using config file
|
||||
confFileHash string
|
||||
serveConfig *ipn.ServeConfig
|
||||
shouldEnableForwardingClusterTrafficViaIngress bool
|
||||
proxyClass string // configuration from the named ProxyClass should be applied to proxy resources
|
||||
}
|
||||
|
||||
func expectedSTS(opts configOpts) *appsv1.StatefulSet {
|
||||
func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.StatefulSet {
|
||||
t.Helper()
|
||||
tsContainer := corev1.Container{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "POD_IP", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "status.podIP"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
|
||||
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
@@ -63,6 +69,12 @@ func expectedSTS(opts configOpts) *appsv1.StatefulSet {
|
||||
},
|
||||
ImagePullPolicy: "Always",
|
||||
}
|
||||
if opts.shouldEnableForwardingClusterTrafficViaIngress {
|
||||
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
|
||||
Name: "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS",
|
||||
Value: "true",
|
||||
})
|
||||
}
|
||||
annots := make(map[string]string)
|
||||
var volumes []corev1.Volume
|
||||
if opts.shouldUseDeclarativeConfig {
|
||||
@@ -122,7 +134,17 @@ func expectedSTS(opts configOpts) *appsv1.StatefulSet {
|
||||
})
|
||||
annots["tailscale.com/operator-last-set-cluster-ip"] = opts.clusterTargetIP
|
||||
}
|
||||
return &appsv1.StatefulSet{
|
||||
if opts.serveConfig != nil {
|
||||
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
|
||||
Name: "TS_SERVE_CONFIG",
|
||||
Value: "/etc/tailscaled/serve-config",
|
||||
})
|
||||
volumes = append(volumes, corev1.Volume{
|
||||
Name: "serve-config", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName, Items: []corev1.KeyToPath{{Path: "serve-config", Key: "serve-config"}}}},
|
||||
})
|
||||
tsContainer.VolumeMounts = append(tsContainer.VolumeMounts, corev1.VolumeMount{Name: "serve-config", ReadOnly: true, MountPath: "/etc/tailscaled"})
|
||||
}
|
||||
ss := &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StatefulSet",
|
||||
APIVersion: "apps/v1",
|
||||
@@ -175,9 +197,92 @@ func expectedSTS(opts configOpts) *appsv1.StatefulSet {
|
||||
},
|
||||
},
|
||||
}
|
||||
// If opts.proxyClass is set, retrieve the ProxyClass and apply
|
||||
// configuration from that to the StatefulSet.
|
||||
if opts.proxyClass != "" {
|
||||
t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
|
||||
proxyClass := new(tsapi.ProxyClass)
|
||||
if err := cl.Get(context.Background(), types.NamespacedName{Name: opts.proxyClass}, proxyClass); err != nil {
|
||||
t.Fatalf("error getting ProxyClass: %v", err)
|
||||
}
|
||||
return applyProxyClassToStatefulSet(proxyClass, ss)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func expectedHeadlessService(name string) *corev1.Service {
|
||||
func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *appsv1.StatefulSet {
|
||||
tsContainer := corev1.Container{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "true"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
|
||||
{Name: "TS_HOSTNAME", Value: opts.hostname},
|
||||
{Name: "TS_SERVE_CONFIG", Value: "/etc/tailscaled/serve-config"},
|
||||
},
|
||||
ImagePullPolicy: "Always",
|
||||
VolumeMounts: []corev1.VolumeMount{{Name: "serve-config", ReadOnly: true, MountPath: "/etc/tailscaled"}},
|
||||
}
|
||||
annots := make(map[string]string)
|
||||
volumes := []corev1.Volume{{Name: "serve-config", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName, Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}}}}}}
|
||||
annots["tailscale.com/operator-last-set-hostname"] = opts.hostname
|
||||
ss := &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StatefulSet",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: opts.stsName,
|
||||
Namespace: "operator-ns",
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": opts.namespace,
|
||||
"tailscale.com/parent-resource-type": opts.parentType,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": "1234-UID"},
|
||||
},
|
||||
ServiceName: opts.stsName,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: annots,
|
||||
DeletionGracePeriodSeconds: ptr.To[int64](10),
|
||||
Labels: map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": opts.namespace,
|
||||
"tailscale.com/parent-resource-type": opts.parentType,
|
||||
"app": "1234-UID",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: "proxies",
|
||||
PriorityClassName: opts.priorityClassName,
|
||||
Containers: []corev1.Container{tsContainer},
|
||||
Volumes: volumes,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
// If opts.proxyClass is set, retrieve the ProxyClass and apply
|
||||
// configuration from that to the StatefulSet.
|
||||
if opts.proxyClass != "" {
|
||||
t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
|
||||
proxyClass := new(tsapi.ProxyClass)
|
||||
if err := cl.Get(context.Background(), types.NamespacedName{Name: opts.proxyClass}, proxyClass); err != nil {
|
||||
t.Fatalf("error getting ProxyClass: %v", err)
|
||||
}
|
||||
return applyProxyClassToStatefulSet(proxyClass, ss)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func expectedHeadlessService(name string, parentType string) *corev1.Service {
|
||||
return &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
@@ -191,7 +296,7 @@ func expectedHeadlessService(name string) *corev1.Service {
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
"tailscale.com/parent-resource-ns": "default",
|
||||
"tailscale.com/parent-resource-type": "svc",
|
||||
"tailscale.com/parent-resource-type": parentType,
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
@@ -220,6 +325,13 @@ func expectedSecret(t *testing.T, opts configOpts) *corev1.Secret {
|
||||
Namespace: "operator-ns",
|
||||
},
|
||||
}
|
||||
if opts.serveConfig != nil {
|
||||
serveConfigBs, err := json.Marshal(opts.serveConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling serve config: %v", err)
|
||||
}
|
||||
mak.Set(&s.StringData, "serve-config", string(serveConfigBs))
|
||||
}
|
||||
if !opts.shouldUseDeclarativeConfig {
|
||||
mak.Set(&s.StringData, "authkey", "secret-authkey")
|
||||
labels["tailscale.com/parent-resource-ns"] = opts.namespace
|
||||
@@ -384,6 +496,13 @@ type fakeTSClient struct {
|
||||
keyRequests []tailscale.KeyCapabilities
|
||||
deleted []string
|
||||
}
|
||||
type fakeTSNetServer struct {
|
||||
certDomains []string
|
||||
}
|
||||
|
||||
func (f *fakeTSNetServer) CertDomains() []string {
|
||||
return f.certDomains
|
||||
}
|
||||
|
||||
func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error) {
|
||||
c.Lock()
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"inet.af/tcpproxy"
|
||||
"github.com/inetaf/tcpproxy"
|
||||
"tailscale.com/net/netutil"
|
||||
)
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/types/structs from tailscale.com/tailcfg+
|
||||
tailscale.com/types/tkatype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/views from tailscale.com/net/tsaddr+
|
||||
tailscale.com/util/cmpx from tailscale.com/tailcfg+
|
||||
tailscale.com/util/ctxkey from tailscale.com/tsweb+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/tailcfg
|
||||
@@ -97,7 +96,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
cmp from slices
|
||||
cmp from slices+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from google.golang.org/protobuf/internal/impl+
|
||||
container/list from crypto/tls+
|
||||
@@ -144,7 +143,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
html from net/http/pprof+
|
||||
io from bufio+
|
||||
io/fs from crypto/x509+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
io/ioutil from google.golang.org/protobuf/internal/impl
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
maps from tailscale.com/tailcfg+
|
||||
|
||||
@@ -147,6 +147,8 @@ change in the future.
|
||||
switch {
|
||||
case slices.Contains(args, "debug"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
case slices.Contains(args, "share"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, shareCmd)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
|
||||
@@ -5,6 +5,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
stdcmp "cmp"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -24,7 +25,6 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -758,7 +758,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf tstest.MemLogger
|
||||
goos := cmpx.Or(tt.goos, "linux")
|
||||
goos := stdcmp.Or(tt.goos, "linux")
|
||||
st := tt.st
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
@@ -802,7 +802,7 @@ func TestPrefFlagMapping(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
prefType := reflect.TypeFor[ipn.Prefs]()
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
if prefHasFlag[prefName] {
|
||||
|
||||
209
cmd/tailscale/cli/share.go
Normal file
209
cmd/tailscale/cli/share.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tailfs"
|
||||
)
|
||||
|
||||
const (
|
||||
shareAddUsage = "share add <name> <path>"
|
||||
shareRemoveUsage = "share remove <name>"
|
||||
shareListUsage = "share list"
|
||||
)
|
||||
|
||||
var shareCmd = &ffcli.Command{
|
||||
Name: "share",
|
||||
ShortHelp: "Share a directory with your tailnet",
|
||||
ShortUsage: strings.Join([]string{
|
||||
shareAddUsage,
|
||||
shareRemoveUsage,
|
||||
shareListUsage,
|
||||
}, "\n "),
|
||||
LongHelp: buildShareLongHelp(),
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Exec: runShareAdd,
|
||||
ShortHelp: "[ALPHA] add a share",
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
ShortHelp: "[ALPHA] remove a share",
|
||||
Exec: runShareRemove,
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
ShortHelp: "[ALPHA] list current shares",
|
||||
Exec: runShareList,
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
return errors.New("share subcommand required; run 'tailscale share -h' for details")
|
||||
},
|
||||
}
|
||||
|
||||
// runShareAdd is the entry point for the "tailscale share add" command.
|
||||
func runShareAdd(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareAddUsage)
|
||||
}
|
||||
|
||||
name, path := args[0], args[1]
|
||||
|
||||
err := localClient.TailFSShareAdd(ctx, &tailfs.Share{
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
if err == nil {
|
||||
fmt.Printf("Added share %q at %q\n", name, path)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// runShareRemove is the entry point for the "tailscale share remove" command.
|
||||
func runShareRemove(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareRemoveUsage)
|
||||
}
|
||||
name := args[0]
|
||||
|
||||
err := localClient.TailFSShareRemove(ctx, name)
|
||||
if err == nil {
|
||||
fmt.Printf("Removed share %q\n", name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// runShareList is the entry point for the "tailscale share list" command.
|
||||
func runShareList(ctx context.Context, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("usage: tailscale %v", shareListUsage)
|
||||
}
|
||||
|
||||
sharesMap, err := localClient.TailFSShareList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shares := make([]*tailfs.Share, 0, len(sharesMap))
|
||||
for _, share := range sharesMap {
|
||||
shares = append(shares, share)
|
||||
}
|
||||
|
||||
sort.Slice(shares, func(i, j int) bool {
|
||||
return shares[i].Name < shares[j].Name
|
||||
})
|
||||
|
||||
longestName := 4 // "name"
|
||||
longestPath := 4 // "path"
|
||||
longestAs := 2 // "as"
|
||||
for _, share := range shares {
|
||||
if len(share.Name) > longestName {
|
||||
longestName = len(share.Name)
|
||||
}
|
||||
if len(share.Path) > longestPath {
|
||||
longestPath = len(share.Path)
|
||||
}
|
||||
if len(share.As) > longestAs {
|
||||
longestAs = len(share.As)
|
||||
}
|
||||
}
|
||||
formatString := fmt.Sprintf("%%-%ds %%-%ds %%s\n", longestName, longestPath)
|
||||
fmt.Printf(formatString, "name", "path", "as")
|
||||
fmt.Printf(formatString, strings.Repeat("-", longestName), strings.Repeat("-", longestPath), strings.Repeat("-", longestAs))
|
||||
for _, share := range shares {
|
||||
fmt.Printf(formatString, share.Name, share.Path, share.As)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildShareLongHelp() string {
|
||||
longHelpAs := ""
|
||||
if tailfs.AllowShareAs() {
|
||||
longHelpAs = shareLongHelpAs
|
||||
}
|
||||
return fmt.Sprintf(shareLongHelpBase, longHelpAs)
|
||||
}
|
||||
|
||||
var shareLongHelpBase = `Tailscale share allows you to share directories with other machines on your tailnet.
|
||||
|
||||
Each share is identified by a name and points to a directory at a specific path. For example, to share the path /Users/me/Documents under the name "docs", you would run:
|
||||
|
||||
$ tailscale share add docs /Users/me/Documents
|
||||
|
||||
Note that the system forces share names to lowercase to avoid problems with clients that don't support case-sensitive filenames.
|
||||
|
||||
Share names may only contain the letters a-z, underscore _, parentheses (), or spaces. Leading and trailing spaces are omitted.
|
||||
|
||||
All Tailscale shares have a globally unique path consisting of the tailnet, the machine name and the share name. For example, if the above share was created on the machine "mylaptop" on the tailnet "mydomain.com", the share's path would be:
|
||||
|
||||
/mydomain.com/mylaptop/docs
|
||||
|
||||
In order to access this share, other machines on the tailnet can connect to the above path on a WebDAV server running at 100.100.100.100:8080, for example:
|
||||
|
||||
http://100.100.100.100:8080/mydomain.com/mylaptop/docs
|
||||
|
||||
Permissions to access shares are controlled via ACLs. For example, to give yourself read/write access and give the group "home" read-only access to the above share, use the below ACL grants:
|
||||
|
||||
{
|
||||
"src": ["mylogin@domain.com"],
|
||||
"dst": ["mylaptop's ip address"],
|
||||
"app": {
|
||||
"tailscale.com/cap/tailfs": [{
|
||||
"shares": ["docs"],
|
||||
"access": "rw"
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"src": ["group:home"],
|
||||
"dst": ["mylaptop"],
|
||||
"app": {
|
||||
"tailscale.com/cap/tailfs": [{
|
||||
"shares": ["docs"],
|
||||
"access": "ro"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
To categorically give yourself access to all your shares, you can use the below ACL grant:
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self"],
|
||||
"app": {
|
||||
"tailscale.com/cap/tailfs": [{
|
||||
"shares": ["*"],
|
||||
"access": "rw"
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Whenever either you or anyone in the group "home" connects to the share, they connect as if they are using your local machine user. They'll be able to read the same files as your user and if they create files, those files will be owned by your user.%s
|
||||
|
||||
You can remove shares by name, for example you could remove the above share by running:
|
||||
|
||||
$ tailscale share remove docs
|
||||
|
||||
You can get a list of currently published shares by running:
|
||||
|
||||
$ tailscale share list`
|
||||
|
||||
var shareLongHelpAs = `
|
||||
|
||||
If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run:
|
||||
|
||||
$ sudo -u theuser tailscale share add docs /Users/theuser/Documents`
|
||||
@@ -5,6 +5,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -23,7 +24,6 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -228,8 +228,8 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
Stdout.Write(buf.Bytes())
|
||||
if locBasedExitNode {
|
||||
println()
|
||||
println("# To see the full list of exit nodes, including location-based exit nodes, run `tailscale exit-node list` \n")
|
||||
outln()
|
||||
printf("# To see the full list of exit nodes, including location-based exit nodes, run `tailscale exit-node list` \n")
|
||||
}
|
||||
if len(st.Health) > 0 {
|
||||
outln()
|
||||
@@ -316,7 +316,7 @@ func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
// their netmap. We've historically (2021-01..2023-08) always shown the
|
||||
// sharer's name in the UI. Perhaps we want to show both here? But the CLI's
|
||||
// a bit space constrained.
|
||||
uid := cmpx.Or(ps.AltSharerUserID, ps.UserID)
|
||||
uid := cmp.Or(ps.AltSharerUserID, ps.UserID)
|
||||
if uid.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ func init() {
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
prefsOfFlag[flagName] = prefNames
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
prefType := reflect.TypeFor[ipn.Prefs]()
|
||||
for _, pref := range prefNames {
|
||||
t := prefType
|
||||
for _, name := range strings.Split(pref, ".") {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/web"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
@@ -42,15 +42,17 @@ Tailscale, as opposed to a CLI or a native app.
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
webf.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
|
||||
webf.BoolVar(&webArgs.readonly, "readonly", false, "run web UI in read-only mode")
|
||||
return webf
|
||||
})(),
|
||||
Exec: runWeb,
|
||||
}
|
||||
|
||||
var webArgs struct {
|
||||
listen string
|
||||
cgi bool
|
||||
prefix string
|
||||
listen string
|
||||
cgi bool
|
||||
prefix string
|
||||
readonly bool
|
||||
}
|
||||
|
||||
func tlsConfigFromEnvironment() *tls.Config {
|
||||
@@ -94,20 +96,26 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
if prefs, err := localClient.GetPrefs(ctx); err == nil {
|
||||
existingWebClient = prefs.RunWebClient
|
||||
}
|
||||
if !existingWebClient {
|
||||
var startedManagementClient bool // we started the management client
|
||||
if !existingWebClient && !webArgs.readonly {
|
||||
// Also start full client in tailscaled.
|
||||
log.Printf("starting tailscaled web client at %s:%d\n", selfIP.String(), web.ListenPort)
|
||||
log.Printf("starting tailscaled web client at http://%s\n", netip.AddrPortFrom(selfIP, web.ListenPort))
|
||||
if err := setRunWebClient(ctx, true); err != nil {
|
||||
return fmt.Errorf("starting web client in tailscaled: %w", err)
|
||||
}
|
||||
startedManagementClient = true
|
||||
}
|
||||
|
||||
webServer, err := web.NewServer(web.ServerOpts{
|
||||
opts := web.ServerOpts{
|
||||
Mode: web.LoginServerMode,
|
||||
CGIMode: webArgs.cgi,
|
||||
PathPrefix: webArgs.prefix,
|
||||
LocalClient: &localClient,
|
||||
})
|
||||
}
|
||||
if webArgs.readonly {
|
||||
opts.Mode = web.ReadOnlyServerMode
|
||||
}
|
||||
webServer, err := web.NewServer(opts)
|
||||
if err != nil {
|
||||
log.Printf("tailscale.web: %v", err)
|
||||
return err
|
||||
@@ -117,10 +125,10 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
case <-ctx.Done():
|
||||
// Shutdown the server.
|
||||
webServer.Shutdown()
|
||||
if !webArgs.cgi && !existingWebClient {
|
||||
if !webArgs.cgi && startedManagementClient {
|
||||
log.Println("stopping tailscaled web client")
|
||||
// When not in cgi mode, shut down the tailscaled
|
||||
// web client on cli termination.
|
||||
// web client on cli termination if we started it.
|
||||
if err := setRunWebClient(context.Background(), false); err != nil {
|
||||
log.Printf("stopping tailscaled web client: %v", err)
|
||||
}
|
||||
@@ -160,5 +168,5 @@ func setRunWebClient(ctx context.Context, val bool) error {
|
||||
// urlOfListenAddr parses a given listen address into a formatted URL
|
||||
func urlOfListenAddr(addr string) string {
|
||||
host, port, _ := net.SplitHostPort(addr)
|
||||
return fmt.Sprintf("http://%s", net.JoinHostPort(cmpx.Or(host, "127.0.0.1"), port))
|
||||
return fmt.Sprintf("http://%s", net.JoinHostPort(cmp.Or(host, "127.0.0.1"), port))
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
L github.com/coreos/go-systemd/v22/dbus from tailscale.com/clientupdate
|
||||
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil/authenticode+
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus
|
||||
@@ -18,17 +18,17 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from tailscale.com/util/quarantine+
|
||||
github.com/google/uuid from tailscale.com/clientupdate+
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
@@ -58,11 +58,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter+
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/net/tsaddr+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket from tailscale.com/control/controlhttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/util from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
@@ -71,11 +71,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
software.sslmate.com/src/go-pkcs12 from tailscale.com/cmd/tailscale/cli
|
||||
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/atomicfile from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/web from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/clientupdate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
@@ -84,54 +84,55 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/net/tlsdial
|
||||
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/licenses from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/licenses from tailscale.com/client/web+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netmon from tailscale.com/net/sockstats+
|
||||
tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/capture+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tlsdial from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/lazy from tailscale.com/version+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/lazy from tailscale.com/util/testenv+
|
||||
tailscale.com/types/logger from tailscale.com/client/web+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/nettype from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
@@ -141,35 +142,34 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
|
||||
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/ctxkey from tailscale.com/types/logger
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
|
||||
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/must from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/set from tailscale.com/derp+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dns/recursive+
|
||||
tailscale.com/util/syspolicy from tailscale.com/ipn
|
||||
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/truncate from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version from tailscale.com/client/web+
|
||||
tailscale.com/version/distro from tailscale.com/client/web+
|
||||
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase+
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
|
||||
golang.org/x/crypto/blake2s from tailscale.com/clientupdate/distsign+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
@@ -189,18 +189,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.org/x/net/icmp+
|
||||
golang.org/x/net/ipv6 from golang.org/x/net/icmp+
|
||||
golang.org/x/net/ipv4 from github.com/miekg/dns+
|
||||
golang.org/x/net/ipv6 from github.com/miekg/dns+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
|
||||
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/sys/cpu from github.com/josharian/native+
|
||||
LD golang.org/x/sys/unix from github.com/google/nftables+
|
||||
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/registry from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
@@ -210,14 +210,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
archive/tar from tailscale.com/clientupdate
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
bytes from archive/tar+
|
||||
cmp from slices+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http+
|
||||
compress/zlib from image/png+
|
||||
compress/zlib from debug/pe+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto from crypto/ecdh+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
@@ -235,16 +235,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/tls from github.com/miekg/dns+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
database/sql/driver from github.com/google/uuid
|
||||
W debug/dwarf from debug/pe
|
||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/gob+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from tailscale.com/tka+
|
||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/gob from github.com/gorilla/securecookie
|
||||
@@ -252,64 +252,64 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
errors from archive/tar+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v3+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
fmt from archive/tar+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
html from html/template+
|
||||
html/template from github.com/gorilla/csrf
|
||||
image from github.com/skip2/go-qrcode+
|
||||
image/color from github.com/skip2/go-qrcode+
|
||||
image/png from github.com/skip2/go-qrcode
|
||||
io from bufio+
|
||||
io/fs from crypto/x509+
|
||||
io from archive/tar+
|
||||
io/fs from archive/tar+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
maps from tailscale.com/types/views+
|
||||
math from compress/flate+
|
||||
maps from tailscale.com/clientupdate+
|
||||
math from archive/tar+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from math/big+
|
||||
mime from mime/multipart+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/httputil from tailscale.com/cmd/tailscale/cli+
|
||||
net/http/httputil from tailscale.com/client/web+
|
||||
net/http/internal from net/http+
|
||||
net/netip from net+
|
||||
net/netip from go4.org/netipx+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember+
|
||||
path from html/template+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
os/user from archive/tar+
|
||||
path from archive/tar+
|
||||
path/filepath from archive/tar+
|
||||
reflect from archive/tar+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from tailscale.com/util/singleflight+
|
||||
runtime/debug from nhooyr.io/websocket/internal/xsync+
|
||||
runtime/trace from testing
|
||||
slices from tailscale.com/cmd/tailscale/cli+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
slices from tailscale.com/client/web+
|
||||
sort from archive/tar+
|
||||
strconv from archive/tar+
|
||||
strings from archive/tar+
|
||||
sync from archive/tar+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
syscall from archive/tar+
|
||||
testing from tailscale.com/util/syspolicy
|
||||
text/tabwriter from github.com/peterbourgon/ff/v3/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
time from archive/tar+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
|
||||
@@ -6,7 +6,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/ssh
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/defaults+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
@@ -87,10 +87,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
|
||||
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
|
||||
💣 github.com/djherbis/times from tailscale.com/tailfs/tailfsimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
@@ -102,12 +103,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/google/uuid from tailscale.com/clientupdate
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
github.com/jellydator/ttlcache/v3 from tailscale.com/tailfs/tailfsimpl/webdavfs
|
||||
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||
@@ -115,14 +117,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd+
|
||||
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/huff0+
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/kr/fs from github.com/pkg/sftp
|
||||
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
@@ -136,7 +138,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
|
||||
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf+
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
@@ -153,9 +155,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tailscale/gowebdav from tailscale.com/tailfs/tailfsimpl/webdavfs
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
|
||||
github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
|
||||
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
|
||||
W 💣 github.com/tailscale/wf from tailscale.com/wf
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/net/tstun+
|
||||
@@ -166,6 +171,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/xnet/webdav from tailscale.com/tailfs/tailfsimpl+
|
||||
github.com/tailscale/xnet/webdav/internal/xml from github.com/tailscale/xnet/webdav
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -173,11 +180,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
💣 go4.org/mem from tailscale.com/control/controlbase+
|
||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from github.com/tailscale/wf+
|
||||
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
@@ -189,9 +196,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
@@ -207,20 +214,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/internal/network+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/raw+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/udp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnauth
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket from tailscale.com/control/controlhttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/util from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
@@ -228,181 +233,186 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/client/tailscale from tailscale.com/derp+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/client/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlclient+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
|
||||
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
||||
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
|
||||
tailscale.com/licenses from tailscale.com/client/web
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy+
|
||||
tailscale.com/logtail from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/connstats from tailscale.com/net/tstun+
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/net/dns
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns+
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun/table from tailscale.com/net/tstun
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/smallzstd from tailscale.com/control/controlclient+
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tailfs from tailscale.com/client/tailscale+
|
||||
tailscale.com/tailfs/tailfsimpl from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/tailfs/tailfsimpl/compositefs from tailscale.com/tailfs/tailfsimpl
|
||||
tailscale.com/tailfs/tailfsimpl/shared from tailscale.com/tailfs/tailfsimpl+
|
||||
tailscale.com/tailfs/tailfsimpl/webdavfs from tailscale.com/tailfs/tailfsimpl
|
||||
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/tstime from tailscale.com/control/controlclient+
|
||||
tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tstime/rate from tailscale.com/derp+
|
||||
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/ipn+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/control/controlbase+
|
||||
tailscale.com/types/lazy from tailscale.com/version+
|
||||
tailscale.com/types/logger from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/logid from tailscale.com/logtail+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/netlogtype from tailscale.com/net/connstats+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/types/nettype from tailscale.com/ipn/localapi+
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/ptr from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/tkatype from tailscale.com/tka+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
|
||||
tailscale.com/util/ctxkey from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/dnsname from tailscale.com/appc+
|
||||
tailscale.com/util/execqueue from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth+
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web+
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/logpolicy+
|
||||
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/must from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/osuser from tailscale.com/ssh/tailssh+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/osuser from tailscale.com/ipn/localapi+
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/set from tailscale.com/derp+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dns/recursive+
|
||||
tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
|
||||
tailscale.com/util/uniq from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/client/web+
|
||||
tailscale.com/version/distro from tailscale.com/client/web+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/netlog from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf+
|
||||
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
@@ -413,9 +423,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
LD golang.org/x/crypto/ssh from github.com/pkg/sftp+
|
||||
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
|
||||
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock+
|
||||
golang.org/x/exp/maps from tailscale.com/appc+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
@@ -425,15 +435,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/conn+
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/conn+
|
||||
golang.org/x/net/ipv4 from github.com/miekg/dns+
|
||||
golang.org/x/net/ipv6 from github.com/miekg/dns+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
|
||||
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/sync/singleflight from github.com/jellydator/ttlcache/v3
|
||||
golang.org/x/sys/cpu from github.com/josharian/native+
|
||||
LD golang.org/x/sys/unix from github.com/google/nftables+
|
||||
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/registry from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled+
|
||||
@@ -442,18 +453,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/log+
|
||||
archive/tar from tailscale.com/clientupdate
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
bytes from archive/tar+
|
||||
cmp from slices+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from golang.org/x/net/http2+
|
||||
W compress/zlib from debug/pe
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/heap from github.com/jellydator/ttlcache/v3+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
crypto from crypto/ecdh+
|
||||
crypto/aes from crypto/ecdsa+
|
||||
crypto/cipher from crypto/aes+
|
||||
crypto/des from crypto/tls+
|
||||
@@ -471,46 +482,47 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/sha256 from crypto/tls+
|
||||
crypto/sha512 from crypto/ecdsa+
|
||||
crypto/subtle from crypto/aes+
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
database/sql/driver from github.com/google/uuid
|
||||
W debug/dwarf from debug/pe
|
||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||
embed from tailscale.com+
|
||||
encoding from encoding/json+
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/gob+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from tailscale.com/tka+
|
||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/gob from github.com/gorilla/securecookie
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
|
||||
errors from archive/tar+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from net/http/httptest+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
fmt from archive/tar+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnlocal+
|
||||
html from html/template+
|
||||
html/template from github.com/gorilla/csrf
|
||||
io from bufio+
|
||||
io/fs from crypto/x509+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
io from archive/tar+
|
||||
io/fs from archive/tar+
|
||||
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
LD log/syslog from tailscale.com/ssh/tailssh
|
||||
maps from tailscale.com/types/views+
|
||||
math from compress/flate+
|
||||
maps from tailscale.com/clientupdate+
|
||||
math from archive/tar+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from mime/multipart+
|
||||
math/rand/v2 from tailscale.com/util/rands
|
||||
mime from github.com/tailscale/xnet/webdav+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
@@ -521,32 +533,32 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
net/http/internal from net/http+
|
||||
net/http/pprof from tailscale.com/cmd/tailscaled+
|
||||
net/netip from github.com/tailscale/wireguard-go/conn+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
|
||||
os/signal from tailscale.com/cmd/tailscaled
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from github.com/godbus/dbus/v5+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
os/user from archive/tar+
|
||||
path from archive/tar+
|
||||
path/filepath from archive/tar+
|
||||
reflect from archive/tar+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from tailscale.com/ipn/ipnlocal+
|
||||
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
|
||||
runtime/pprof from net/http/pprof+
|
||||
runtime/trace from net/http/pprof+
|
||||
slices from tailscale.com/wgengine/magicsock+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
slices from tailscale.com/appc+
|
||||
sort from archive/tar+
|
||||
strconv from archive/tar+
|
||||
strings from archive/tar+
|
||||
sync from archive/tar+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
syscall from archive/tar+
|
||||
testing from tailscale.com/util/syspolicy
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
time from archive/tar+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/types/flagtype"
|
||||
@@ -135,11 +136,16 @@ var (
|
||||
createBIRDClient func(string) (wgengine.BIRDClient, error) // non-nil on some platforms
|
||||
)
|
||||
|
||||
// Note - we use function pointers for subcommands so that subcommands like
|
||||
// installSystemDaemon and uninstallSystemDaemon can be assigned platform-
|
||||
// specific variants.
|
||||
|
||||
var subCommands = map[string]*func([]string) error{
|
||||
"install-system-daemon": &installSystemDaemon,
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
"be-child": &beChildFunc,
|
||||
"serve-tailfs": &serveTailFSFunc,
|
||||
}
|
||||
|
||||
var beCLI func() // non-nil if CLI is linked in
|
||||
@@ -172,7 +178,7 @@ func main() {
|
||||
if len(os.Args) > 1 {
|
||||
sub := os.Args[1]
|
||||
if fp, ok := subCommands[sub]; ok {
|
||||
if *fp == nil {
|
||||
if fp == nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
|
||||
}
|
||||
@@ -401,6 +407,8 @@ func run() (err error) {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
sys.Set(tailfsimpl.NewFileSystemForRemote(logf))
|
||||
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
|
||||
}
|
||||
|
||||
@@ -623,11 +631,12 @@ var tstunNew = tstun.New
|
||||
|
||||
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
ListenPort: args.port,
|
||||
NetMon: sys.NetMon.Get(),
|
||||
Dialer: sys.Dialer.Get(),
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
TailFSForLocal: tailfsimpl.NewFileSystemForLocal(logf),
|
||||
}
|
||||
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
@@ -730,6 +739,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
|
||||
tfs, _ := sys.TailFSForLocal.GetOK()
|
||||
ret, err := netstack.Create(logf,
|
||||
sys.Tun.Get(),
|
||||
sys.Engine.Get(),
|
||||
@@ -737,6 +747,7 @@ func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
|
||||
sys.Dialer.Get(),
|
||||
sys.DNSManager.Get(),
|
||||
sys.ProxyMapper(),
|
||||
tfs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -806,6 +817,35 @@ func beChild(args []string) error {
|
||||
return f(args[1:])
|
||||
}
|
||||
|
||||
var serveTailFSFunc = serveTailFS
|
||||
|
||||
// serveTailFS serves one or more tailfs on localhost using the WebDAV
|
||||
// protocol. On UNIX and MacOS tailscaled environment, tailfs spawns child
|
||||
// tailscaled processes in serve-tailfs mode in order to access the fliesystem
|
||||
// as specific (usually unprivileged) users.
|
||||
//
|
||||
// serveTailFS prints the address on which it's listening to stdout so that the
|
||||
// parent process knows where to connect to.
|
||||
func serveTailFS(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("missing shares")
|
||||
}
|
||||
if len(args)%2 != 0 {
|
||||
return errors.New("need <sharename> <path> pairs")
|
||||
}
|
||||
s, err := tailfsimpl.NewFileServer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start tailfs FileServer: %v", err)
|
||||
}
|
||||
shares := make(map[string]string)
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
shares[args[i]] = args[i+1]
|
||||
}
|
||||
s.SetShares(shares)
|
||||
fmt.Printf("%v\n", s.Addr())
|
||||
return s.Serve()
|
||||
}
|
||||
|
||||
// dieOnPipeReadErrorOfFD reads from the pipe named by fd and exit the process
|
||||
// when the pipe becomes readable. We use this in tests as a somewhat more
|
||||
// portable mechanism for the Linux PR_SET_PDEATHSIG, which we wish existed on
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailfs/tailfsimpl"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
@@ -315,6 +316,8 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
sys.Set(netMon)
|
||||
|
||||
sys.Set(tailfsimpl.NewFileSystemForRemote(log.Printf))
|
||||
|
||||
publicLogID, _ := logid.ParsePublicID(logID)
|
||||
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
|
||||
if err != nil {
|
||||
|
||||
@@ -110,7 +110,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
}
|
||||
sys.Set(eng)
|
||||
|
||||
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
|
||||
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestStatusEqual(t *testing.T) {
|
||||
// Verify that the Equal method stays in sync with reality
|
||||
equalHandles := []string{"Err", "URL", "NetMap", "Persist", "state"}
|
||||
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
|
||||
if have := fieldsOf(reflect.TypeFor[Status]()); !reflect.DeepEqual(have, equalHandles) {
|
||||
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, equalHandles)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -26,7 +27,6 @@ import (
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -130,7 +130,7 @@ func (ms *mapSession) occasionallyPrintSummary(summary string) {
|
||||
}
|
||||
|
||||
func (ms *mapSession) clock() tstime.Clock {
|
||||
return cmpx.Or[tstime.Clock](ms.altClock, tstime.StdClock{})
|
||||
return cmp.Or[tstime.Clock](ms.altClock, tstime.StdClock{})
|
||||
}
|
||||
|
||||
func (ms *mapSession) Close() {
|
||||
@@ -171,7 +171,7 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
}
|
||||
|
||||
// Call Node.InitDisplayNames on any changed nodes.
|
||||
initDisplayNames(cmpx.Or(resp.Node.View(), ms.lastNode), resp)
|
||||
initDisplayNames(cmp.Or(resp.Node.View(), ms.lastNode), resp)
|
||||
|
||||
ms.patchifyPeersChanged(resp)
|
||||
|
||||
@@ -538,7 +538,7 @@ var nodeFields = sync.OnceValue(getNodeFields)
|
||||
|
||||
// getNodeFields returns the fails of tailcfg.Node.
|
||||
func getNodeFields() []string {
|
||||
rt := reflect.TypeOf((*tailcfg.Node)(nil)).Elem()
|
||||
rt := reflect.TypeFor[tailcfg.Node]()
|
||||
ret := make([]string, rt.NumField())
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
ret[i] = rt.Field(i).Name
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestAsDebugJSON(t *testing.T) {
|
||||
}
|
||||
k := new(Knobs)
|
||||
got := k.AsDebugJSON()
|
||||
if want := reflect.TypeOf(Knobs{}).NumField(); len(got) != want {
|
||||
if want := reflect.TypeFor[Knobs]().NumField(); len(got) != want {
|
||||
t.Errorf("AsDebugJSON map has %d fields; want %v", len(got), want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ package derphttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
@@ -41,7 +42,6 @@ import (
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
// Client is a DERP-over-HTTP client.
|
||||
@@ -701,7 +701,7 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
// Start v4 dial
|
||||
}
|
||||
}
|
||||
dst := cmpx.Or(dstPrimary, n.HostName)
|
||||
dst := cmp.Or(dstPrimary, n.HostName)
|
||||
port := "443"
|
||||
if n.DERPPort != 0 {
|
||||
port = fmt.Sprint(n.DERPPort)
|
||||
|
||||
23
doctor/ethtool/ethtool.go
Normal file
23
doctor/ethtool/ethtool.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ethtool provides a doctor.Check that prints diagnostic information
|
||||
// obtained from the 'ethtool' utility on the current system.
|
||||
package ethtool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "ethtool"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
return ethtoolImpl(logf)
|
||||
}
|
||||
78
doctor/ethtool/ethtool_linux.go
Normal file
78
doctor/ethtool/ethtool_linux.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ethtool
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sort"
|
||||
|
||||
"github.com/safchain/ethtool"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func ethtoolImpl(logf logger.Logf) error {
|
||||
et, err := ethtool.NewEthtool()
|
||||
if err != nil {
|
||||
logf("could not create ethtool: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer et.Close()
|
||||
|
||||
interfaces.ForeachInterface(func(iface interfaces.Interface, _ []netip.Prefix) {
|
||||
ilogf := logger.WithPrefix(logf, iface.Name+": ")
|
||||
features, err := et.Features(iface.Name)
|
||||
if err == nil {
|
||||
enabled := []string{}
|
||||
for feature, value := range features {
|
||||
if value {
|
||||
enabled = append(enabled, feature)
|
||||
}
|
||||
}
|
||||
sort.Strings(enabled)
|
||||
ilogf("features: %v", enabled)
|
||||
} else {
|
||||
ilogf("features: error: %v", err)
|
||||
}
|
||||
|
||||
stats, err := et.Stats(iface.Name)
|
||||
if err == nil {
|
||||
printStats(ilogf, stats)
|
||||
} else {
|
||||
ilogf("stats: error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats that should be printed if non-zero
|
||||
var nonzeroStats = set.SetOf([]string{
|
||||
// AWS ENA driver statistics; see:
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/monitoring-network-performance-ena.html
|
||||
"bw_in_allowance_exceeded",
|
||||
"bw_out_allowance_exceeded",
|
||||
"conntrack_allowance_exceeded",
|
||||
"linklocal_allowance_exceeded",
|
||||
"pps_allowance_exceeded",
|
||||
})
|
||||
|
||||
// Stats that should be printed if zero
|
||||
var zeroStats = set.SetOf([]string{
|
||||
// AWS ENA driver statistics; see:
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/monitoring-network-performance-ena.html
|
||||
"conntrack_allowance_available",
|
||||
})
|
||||
|
||||
func printStats(logf logger.Logf, stats map[string]uint64) {
|
||||
for name, value := range stats {
|
||||
if value != 0 && nonzeroStats.Contains(name) {
|
||||
logf("stats: warning: %s = %d > 0", name, value)
|
||||
}
|
||||
if value == 0 && zeroStats.Contains(name) {
|
||||
logf("stats: warning: %s = %d == 0", name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
doctor/ethtool/ethtool_other.go
Normal file
17
doctor/ethtool/ethtool_other.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package ethtool
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func ethtoolImpl(logf logger.Logf) error {
|
||||
logf("unsupported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
return nil
|
||||
}
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -3,11 +3,11 @@
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -21,11 +21,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -36,11 +36,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1693060755,
|
||||
"narHash": "sha256-KNsbfqewEziFJEpPR0qvVz4rx0x6QXxw1CcunRhlFdk=",
|
||||
"lastModified": 1707619277,
|
||||
"narHash": "sha256-vKnYD5GMQbNQyyQm4wRlqi+5n0/F1hnvqSQgaBy4BqY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c66ccfa00c643751da2fd9290e096ceaa30493fc",
|
||||
"rev": "f3a93440fbfff8a74350f4791332a19282cc6dc8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
# So really, this flake is for tailscale devs to dogfood with, if
|
||||
# you're an end user you should be prepared for this flake to not
|
||||
# build periodically.
|
||||
tailscale = pkgs: pkgs.buildGo121Module rec {
|
||||
tailscale = pkgs: pkgs.buildGo122Module rec {
|
||||
name = "tailscale";
|
||||
|
||||
src = ./.;
|
||||
@@ -112,7 +112,7 @@
|
||||
gotools
|
||||
graphviz
|
||||
perl
|
||||
go_1_21
|
||||
go_1_22
|
||||
yarn
|
||||
];
|
||||
};
|
||||
@@ -120,4 +120,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-b/iffKOn7nMiWvM0AIGGzZaJ15NTaBlJff+aja3NQio=
|
||||
# nix-direnv cache busting line: sha256-1g50+BwoUCwc/tBmnP2KO6e3GwL8QQ/wJ+XoxCzzk3k=
|
||||
|
||||
16
go.mod
16
go.mod
@@ -1,8 +1,6 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.21.1
|
||||
|
||||
toolchain go1.21.5
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
@@ -24,6 +22,7 @@ require (
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/dsnet/try v0.0.3
|
||||
github.com/evanw/esbuild v0.19.11
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
@@ -42,7 +41,9 @@ require (
|
||||
github.com/hdevalence/ed25519consensus v0.2.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/illarion/gonotify v1.0.1
|
||||
github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||
github.com/jsimonetti/rtnetlink v1.4.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
@@ -67,11 +68,15 @@ require (
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/gowebdav v0.0.0-20240130173557-d49b872b5126
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
||||
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
|
||||
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9
|
||||
github.com/tc-hib/winres v0.2.1
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
@@ -96,9 +101,6 @@ require (
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gvisor.dev/gvisor v0.0.0-20240119233241-c9c1d4f9b186
|
||||
honnef.co/go/tools v0.4.6
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9
|
||||
inet.af/wf v0.0.0-20221017222439-36129f591884
|
||||
k8s.io/api v0.29.1
|
||||
k8s.io/apimachinery v0.29.1
|
||||
k8s.io/apiserver v0.29.1
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-b/iffKOn7nMiWvM0AIGGzZaJ15NTaBlJff+aja3NQio=
|
||||
sha256-1g50+BwoUCwc/tBmnP2KO6e3GwL8QQ/wJ+XoxCzzk3k=
|
||||
|
||||
26
go.sum
26
go.sum
@@ -244,6 +244,8 @@ github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20
|
||||
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpFowZBX6GoQ=
|
||||
github.com/docker/cli v25.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
@@ -526,10 +528,14 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c h1:gYfYE403/nlrGNYj6BEOs9ucLCAGB9gstlSk92DttTg=
|
||||
github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM=
|
||||
github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
|
||||
@@ -863,16 +869,24 @@ github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2C
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/gowebdav v0.0.0-20240130173557-d49b872b5126 h1:EBLH+PeC3efXmUi82yEMxjlcKhDwAUZTi0tIT4Q8oTg=
|
||||
github.com/tailscale/gowebdav v0.0.0-20240130173557-d49b872b5126/go.mod h1:UCbnLJ2ebWLs28V9ubpXbq4Qx3e0q1TVoM1AC3Z2b40=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734 h1:93cvKHbvsPK3MKfFTvR00d0b0R0bzRKBW9yrj813fhI=
|
||||
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734/go.mod h1:6v53VHLmLKUaqWMpSGDeRWhltLSCEteMItYoiKLpdJk=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7 h1:xAgOVncJuuxkFZ2oXXDKFTH4HDdFYSZRYdA6oMrCewg=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 h1:G6/VUGQkHbBffO0s3f51DThcHCWrShlWklcS4Zxh5BU=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4CaamMdC3tFoTpzrsUSMDPV0K6rhnQdF7kXekQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
@@ -1134,7 +1148,6 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1145,6 +1158,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1414,12 +1428,6 @@ honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8=
|
||||
honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9 h1:zomTWJvjwLbKRgGameQtpK6DNFUbZ2oNJuWhgUkGp3M=
|
||||
inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=
|
||||
inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q=
|
||||
inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE=
|
||||
k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw=
|
||||
k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ=
|
||||
k8s.io/apiextensions-apiserver v0.29.1 h1:S9xOtyk9M3Sk1tIpQMu9wXHm5O2MX6Y1kIpPMimZBZw=
|
||||
|
||||
@@ -1 +1 @@
|
||||
tailscale.go1.21
|
||||
tailscale.go1.22
|
||||
|
||||
@@ -1 +1 @@
|
||||
ea90ced9ddc95c09aed7d9c59631aa978022c3ba
|
||||
66fe5734c4555397ef1b9de3e1ec958bf0a2086e
|
||||
|
||||
@@ -65,7 +65,8 @@ const (
|
||||
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
|
||||
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
|
||||
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
NotifyInitialTailFSShares // if set, the first Notify message (sent immediately) will contain the current TailFS Shares
|
||||
)
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
@@ -121,6 +122,13 @@ type Notify struct {
|
||||
// is available.
|
||||
ClientVersion *tailcfg.ClientVersion `json:",omitempty"`
|
||||
|
||||
// TailFSShares tracks the full set of current TailFSShares that we're
|
||||
// publishing as name->path. Some client applications, like the MacOS and
|
||||
// Windows clients, will listen for updates to this and handle serving
|
||||
// these shares under the identity of the unprivileged user that is running
|
||||
// the application.
|
||||
TailFSShares map[string]string `json:",omitempty"`
|
||||
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"inet.af/peercred"
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/safesocket"
|
||||
|
||||
@@ -8,7 +8,7 @@ package ipnauth
|
||||
import (
|
||||
"net"
|
||||
|
||||
"inet.af/peercred"
|
||||
"github.com/tailscale/peercred"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestHandleC2NTLSCertStatus(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
handleC2NTLSCertStatus(b, rec, httptest.NewRequest("GET", "/tls-cert-status?domain="+url.QueryEscape(tt.domain), nil))
|
||||
res := rec.Result()
|
||||
wantStatus := cmpx.Or(tt.wantStatus, 200)
|
||||
wantStatus := cmp.Or(tt.wantStatus, 200)
|
||||
if res.StatusCode != wantStatus {
|
||||
t.Fatalf("status code = %v; want %v. Body: %s", res.Status, wantStatus, rec.Body.Bytes())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -330,7 +330,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verOS := cmpx.Or(tt.os, "linux")
|
||||
verOS := cmp.Or(tt.os, "linux")
|
||||
var log tstest.MemLogger
|
||||
got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), log.Logf, verOS)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/doctor"
|
||||
"tailscale.com/doctor/ethtool"
|
||||
"tailscale.com/doctor/permissions"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/envknob"
|
||||
@@ -82,7 +83,6 @@ import (
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -242,8 +242,9 @@ type LocalBackend struct {
|
||||
endpoints []tailcfg.Endpoint
|
||||
blocked bool
|
||||
keyExpired bool
|
||||
authURL string // cleared on Notify
|
||||
authURLSticky string // not cleared on Notify
|
||||
authURL string // cleared on Notify
|
||||
authURLSticky string // not cleared on Notify
|
||||
authURLTime time.Time // when the authURL was received from the control server
|
||||
interact bool
|
||||
egg bool
|
||||
prevIfState *interfaces.State
|
||||
@@ -428,6 +429,17 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
}
|
||||
}
|
||||
|
||||
// initialize TailFS shares from saved state
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
if ok {
|
||||
b.mu.Lock()
|
||||
shares, err := b.tailFSGetSharesLocked()
|
||||
b.mu.Unlock()
|
||||
if err == nil && len(shares) > 0 {
|
||||
fs.SetShares(shares)
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -915,6 +927,7 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.
|
||||
var zero tailcfg.NodeView
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
nid, ok := b.nodeByAddr[ipp.Addr()]
|
||||
if !ok {
|
||||
var ip netip.Addr
|
||||
@@ -1097,6 +1110,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
b.authURLSticky = st.URL
|
||||
b.authURLTime = b.clock.Now()
|
||||
}
|
||||
if (wasBlocked || b.seamlessRenewalEnabled()) && st.LoginFinished() {
|
||||
// Interactive login finished successfully (URL visited).
|
||||
@@ -2253,7 +2267,7 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
b.mu.Lock()
|
||||
b.activeWatchSessions.Add(sessionID)
|
||||
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialTailFSShares
|
||||
if mask&initialBits != 0 {
|
||||
ini = &ipn.Notify{Version: version.Long()}
|
||||
if mask&ipn.NotifyInitialState != 0 {
|
||||
@@ -2269,6 +2283,17 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
if mask&ipn.NotifyInitialNetMap != 0 {
|
||||
ini.NetMap = b.netMap
|
||||
}
|
||||
if mask&ipn.NotifyInitialTailFSShares != 0 && b.tailFSSharingEnabledLocked() {
|
||||
shares, err := b.tailFSGetSharesLocked()
|
||||
if err != nil {
|
||||
b.logf("unable to notify initial tailfs shares: %v", err)
|
||||
} else {
|
||||
ini.TailFSShares = make(map[string]string, len(shares))
|
||||
for _, share := range shares {
|
||||
ini.TailFSShares[share.Name] = share.Path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle := b.notifyWatchers.Add(&watchSession{ch, sessionID})
|
||||
@@ -2785,11 +2810,15 @@ func (b *LocalBackend) StartLoginInteractive() {
|
||||
b.assertClientLocked()
|
||||
b.interact = true
|
||||
url := b.authURL
|
||||
timeSinceAuthURLCreated := b.clock.Since(b.authURLTime)
|
||||
cc := b.cc
|
||||
b.mu.Unlock()
|
||||
b.logf("StartLoginInteractive: url=%v", url != "")
|
||||
|
||||
if url != "" {
|
||||
// Only use an authURL if it was sent down from control in the last
|
||||
// 6 days and 23 hours. Avoids using a stale URL that is no longer valid
|
||||
// server-side. Server-side URLs expire after 7 days.
|
||||
if url != "" && timeSinceAuthURLCreated < ((7*24*time.Hour)-(1*time.Hour)) {
|
||||
b.popBrowserAuthNow()
|
||||
} else {
|
||||
cc.Login(nil, b.loginFlags|controlclient.LoginInteractive)
|
||||
@@ -3194,7 +3223,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
|
||||
if !oldp.Persist().Valid() {
|
||||
b.logf("active login: %s", newLoginName)
|
||||
} else {
|
||||
oldLoginName := oldp.Persist().UserProfile().LoginName()
|
||||
oldLoginName := oldp.Persist().UserProfile().LoginName
|
||||
if oldLoginName != newLoginName {
|
||||
b.logf("active login: %q (changed from %q)", newLoginName, oldLoginName)
|
||||
}
|
||||
@@ -3270,9 +3299,13 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
|
||||
return
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isLocalIP(ip netip.Addr) bool {
|
||||
func (b *LocalBackend) isLocallyAvailable(ip netip.Addr) bool {
|
||||
nm := b.NetMap()
|
||||
return nm != nil && views.SliceContains(nm.GetAddresses(), netip.PrefixFrom(ip, ip.BitLen()))
|
||||
if nm == nil {
|
||||
return false
|
||||
}
|
||||
pfx := netip.PrefixFrom(ip, ip.BitLen())
|
||||
return views.SliceContains(nm.SelfNode.AllowedIPs(), pfx)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -3290,7 +3323,7 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
}
|
||||
return b.HandleQuad100Port80Conn, opts
|
||||
}
|
||||
if !b.isLocalIP(dst.Addr()) {
|
||||
if !b.isLocallyAvailable(dst.Addr()) {
|
||||
return nil, nil
|
||||
}
|
||||
if dst.Port() == 22 && b.ShouldRunSSH() {
|
||||
@@ -3307,6 +3340,18 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
if dst.Port() == webClientPort && b.ShouldRunWebClient() {
|
||||
return b.handleWebClientConn, opts
|
||||
}
|
||||
if dst.Port() == TailFSLocalPort {
|
||||
fs, ok := b.sys.TailFSForLocal.GetOK()
|
||||
if ok {
|
||||
return func(conn net.Conn) error {
|
||||
if !b.TailFSAccessEnabled() {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
return fs.HandleConn(conn, conn.RemoteAddr())
|
||||
}, opts
|
||||
}
|
||||
}
|
||||
if port, ok := b.GetPeerAPIPort(dst.Addr()); ok && dst.Port() == port {
|
||||
return func(c net.Conn) error {
|
||||
b.handlePeerAPIConn(src, dst, c)
|
||||
@@ -4167,6 +4212,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
|
||||
if newState == ipn.Running {
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.authURLTime = time.Time{}
|
||||
} else if oldState == ipn.Running {
|
||||
// Transitioning away from running.
|
||||
b.closePeerAPIListenersLocked()
|
||||
@@ -4409,6 +4455,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.keyExpired = false
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.authURLTime = time.Time{}
|
||||
b.activeLogin = ""
|
||||
b.setAtomicValuesFromPrefsLocked(ipn.PrefsView{})
|
||||
b.enterStateLockedOnEntry(ipn.Stopped)
|
||||
@@ -4527,7 +4574,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
var login string
|
||||
if nm != nil {
|
||||
login = cmpx.Or(nm.UserProfiles[nm.User()].LoginName, "<missing-profile>")
|
||||
login = cmp.Or(nm.UserProfiles[nm.User()].LoginName, "<missing-profile>")
|
||||
}
|
||||
b.netMap = nm
|
||||
b.updatePeersFromNetmapLocked(nm)
|
||||
@@ -4601,6 +4648,11 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
delete(b.nodeByAddr, k)
|
||||
}
|
||||
}
|
||||
|
||||
if b.tailFSSharingEnabledLocked() {
|
||||
b.updateTailFSPeersLocked(nm)
|
||||
b.tailFSNotifyCurrentSharesLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
@@ -4608,14 +4660,17 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
b.peers = nil
|
||||
return
|
||||
}
|
||||
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range b.peers {
|
||||
b.peers[k] = tailcfg.NodeView{}
|
||||
}
|
||||
|
||||
// Second pass, add everything wanted.
|
||||
for _, p := range nm.Peers {
|
||||
mak.Set(&b.peers, p.ID(), p)
|
||||
}
|
||||
|
||||
// Third pass, remove deleted things.
|
||||
for k, v := range b.peers {
|
||||
if !v.Valid() {
|
||||
@@ -4624,6 +4679,28 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// tailFSTransport is an http.RoundTripper that uses the latest value of
|
||||
// b.Dialer().PeerAPITransport() for each round trip and imposes a short
|
||||
// dial timeout to avoid hanging on connecting to offline/unreachable hosts.
|
||||
type tailFSTransport struct {
|
||||
b *LocalBackend
|
||||
}
|
||||
|
||||
func (t *tailFSTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// dialTimeout is fairly aggressive to avoid hangs on contacting offline or
|
||||
// unreachable hosts.
|
||||
dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this
|
||||
|
||||
tr := t.b.Dialer().PeerAPITransport().Clone()
|
||||
dialContext := tr.DialContext
|
||||
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer cancel()
|
||||
return dialContext(ctxWithTimeout, network, addr)
|
||||
}
|
||||
return tr.RoundTrip(req)
|
||||
}
|
||||
|
||||
// setDebugLogsByCapabilityLocked sets debug logging based on the self node's
|
||||
// capabilities in the provided NetMap.
|
||||
func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
|
||||
@@ -5439,6 +5516,7 @@ func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
|
||||
checks = append(checks,
|
||||
permissions.Check{},
|
||||
routetable.Check{},
|
||||
ethtool.Check{},
|
||||
)
|
||||
|
||||
// Print a log message if any of the global DNS resolvers are Tailscale
|
||||
@@ -5808,7 +5886,7 @@ func (b *LocalBackend) AdvertiseRoute(ipps ...netip.Prefix) error {
|
||||
}
|
||||
|
||||
// If the new prefix is already contained by existing routes, skip it.
|
||||
if coveredRouteRange(finalRoutes, ipp) {
|
||||
if coveredRouteRangeNoDefault(finalRoutes, ipp) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -5829,10 +5907,13 @@ func (b *LocalBackend) AdvertiseRoute(ipps ...netip.Prefix) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// coveredRouteRange checks if a route is already included in a slice of
|
||||
// prefixes.
|
||||
func coveredRouteRange(finalRoutes []netip.Prefix, ipp netip.Prefix) bool {
|
||||
// coveredRouteRangeNoDefault checks if a route is already included in a slice of
|
||||
// prefixes, ignoring default routes in the range.
|
||||
func coveredRouteRangeNoDefault(finalRoutes []netip.Prefix, ipp netip.Prefix) bool {
|
||||
for _, r := range finalRoutes {
|
||||
if r == tsaddr.AllIPv4() || r == tsaddr.AllIPv6() {
|
||||
continue
|
||||
}
|
||||
if ipp.IsSingleIP() {
|
||||
if r.Contains(ipp.Addr()) {
|
||||
return true
|
||||
|
||||
@@ -803,7 +803,7 @@ func TestWatchNotificationsCallbacks(t *testing.T) {
|
||||
|
||||
// tests LocalBackend.updateNetmapDeltaLocked
|
||||
func TestUpdateNetmapDelta(t *testing.T) {
|
||||
var b LocalBackend
|
||||
b := newTestLocalBackend(t)
|
||||
if b.updateNetmapDeltaLocked(nil) {
|
||||
t.Errorf("updateNetmapDeltaLocked() = true, want false with nil netmap")
|
||||
}
|
||||
@@ -1218,7 +1218,7 @@ func TestObserveDNSResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoveredRouteRange(t *testing.T) {
|
||||
func TestCoveredRouteRangeNoDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
existingRoute netip.Prefix
|
||||
newRoute netip.Prefix
|
||||
@@ -1244,10 +1244,20 @@ func TestCoveredRouteRange(t *testing.T) {
|
||||
newRoute: netip.MustParsePrefix("192.0.0.0/24"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
existingRoute: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
newRoute: netip.MustParsePrefix("192.0.0.0/24"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
existingRoute: netip.MustParsePrefix("::/0"),
|
||||
newRoute: netip.MustParsePrefix("2001:db8::/32"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := coveredRouteRange([]netip.Prefix{tt.existingRoute}, tt.newRoute)
|
||||
got := coveredRouteRangeNoDefault([]netip.Prefix{tt.existingRoute}, tt.newRoute)
|
||||
if got != tt.want {
|
||||
t.Errorf("coveredRouteRange(%v, %v) = %v, want %v", tt.existingRoute, tt.newRoute, got, tt.want)
|
||||
}
|
||||
|
||||
@@ -38,12 +38,17 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/httphdr"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
const (
|
||||
tailFSPrefix = "/v0/tailfs"
|
||||
)
|
||||
|
||||
var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, string) error
|
||||
|
||||
// addH2C is non-nil on platforms where we want to add H2C
|
||||
@@ -317,6 +322,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleDNSQuery(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, tailFSPrefix) {
|
||||
h.handleServeTailFS(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/v0/goroutines":
|
||||
h.handleServeGoroutines(w, r)
|
||||
@@ -342,6 +351,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
case "/v0/doctor":
|
||||
h.handleServeDoctor(w, r)
|
||||
return
|
||||
case "/v0/sockstats":
|
||||
h.handleServeSockStats(w, r)
|
||||
return
|
||||
@@ -626,7 +636,11 @@ func (h *peerAPIHandler) canIngress() bool {
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
|
||||
return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
|
||||
return h.peerCaps().HasCapability(wantCap)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) peerCaps() tailcfg.PeerCapMap {
|
||||
return h.ps.b.PeerCaps(h.remoteAddr.Addr())
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1054,6 +1068,9 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
||||
return err
|
||||
}
|
||||
if h.Class != dnsmessage.ClassINET {
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch h.Type {
|
||||
@@ -1075,6 +1092,10 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
||||
return err
|
||||
}
|
||||
gotIPs = append(gotIPs, r.TXT...)
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
j, _ := json.Marshal(gotIPs)
|
||||
@@ -1083,6 +1104,39 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeTailFS(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.ps.b.TailFSSharingEnabled() {
|
||||
http.Error(w, "tailfs not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
capsMap := h.peerCaps()
|
||||
tailfsCaps, ok := capsMap[tailcfg.PeerCapabilityTailFS]
|
||||
if !ok {
|
||||
http.Error(w, "tailfs not permitted", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
rawPerms := make([][]byte, 0, len(tailfsCaps))
|
||||
for _, cap := range tailfsCaps {
|
||||
rawPerms = append(rawPerms, []byte(cap))
|
||||
}
|
||||
|
||||
p, err := tailfs.ParsePermissions(rawPerms)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fs, ok := h.ps.b.sys.TailFSForRemote.GetOK()
|
||||
if !ok {
|
||||
http.Error(w, "tailfs not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, tailFSPrefix)
|
||||
fs.ServeHTTPWithPerms(p, w, r)
|
||||
}
|
||||
|
||||
// newFakePeerAPIListener creates a new net.Listener that acts like
|
||||
// it's listening on the provided IP address and on TCP port 1.
|
||||
//
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipnlocal
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -685,6 +686,68 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
// configure as an app connector just to enable the API.
|
||||
appConnector: appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}),
|
||||
},
|
||||
}
|
||||
|
||||
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
|
||||
b.CNAMEResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName("www.example.com."),
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.CNAMEResource{
|
||||
CNAME: dnsmessage.MustNewName("example.com."),
|
||||
},
|
||||
)
|
||||
b.AResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName("example.com."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.AResource{
|
||||
A: [4]byte{192, 0, 0, 8},
|
||||
},
|
||||
)
|
||||
}}
|
||||
f := filter.NewAllowAllForTest(logger.Discard)
|
||||
h.ps.b.setFilter(f)
|
||||
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
var addrs []string
|
||||
json.NewDecoder(w.Body).Decode(&addrs)
|
||||
if len(addrs) == 0 {
|
||||
t.Fatalf("no addresses returned")
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
netip.MustParseAddr(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var h peerAPIHandler
|
||||
@@ -704,7 +767,19 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
|
||||
h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
|
||||
h.ps.resolver = &fakeResolver{}
|
||||
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
|
||||
b.AResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName("example.com."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.AResource{
|
||||
A: [4]byte{192, 0, 0, 8},
|
||||
},
|
||||
)
|
||||
}}
|
||||
f := filter.NewAllowAllForTest(logger.Discard)
|
||||
h.ps.b.setFilter(f)
|
||||
|
||||
@@ -716,7 +791,7 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=true&t=example.com.", nil))
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
@@ -728,22 +803,80 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type fakeResolver struct{}
|
||||
func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
appConnector: appc.NewAppConnector(t.Logf, rc),
|
||||
},
|
||||
}
|
||||
h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
|
||||
h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
|
||||
b.CNAMEResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName("www.example.com."),
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.CNAMEResource{
|
||||
CNAME: dnsmessage.MustNewName("example.com."),
|
||||
},
|
||||
)
|
||||
b.AResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName("example.com."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.AResource{
|
||||
A: [4]byte{192, 0, 0, 8},
|
||||
},
|
||||
)
|
||||
}}
|
||||
f := filter.NewAllowAllForTest(logger.Discard)
|
||||
h.ps.b.setFilter(f)
|
||||
|
||||
if !h.ps.b.OfferingAppConnector() {
|
||||
t.Fatal("expecting to be offering app connector")
|
||||
}
|
||||
if !h.replyToDNSQueries() {
|
||||
t.Errorf("unexpectedly deny; wanted to be a DNS server")
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %v", w.Code)
|
||||
}
|
||||
h.ps.b.appConnector.Wait(ctx)
|
||||
|
||||
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
|
||||
if !slices.Equal(rc.Routes(), wantRoutes) {
|
||||
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeResolver struct {
|
||||
build func(*dnsmessage.Builder)
|
||||
}
|
||||
|
||||
func (f *fakeResolver) HandlePeerDNSQuery(ctx context.Context, q []byte, from netip.AddrPort, allowName func(name string) bool) (res []byte, err error) {
|
||||
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
|
||||
b.EnableCompression()
|
||||
b.StartAnswers()
|
||||
b.AResource(
|
||||
dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName("example.com."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 0,
|
||||
},
|
||||
dnsmessage.AResource{
|
||||
A: [4]byte{192, 0, 0, 8},
|
||||
},
|
||||
)
|
||||
f.build(&b)
|
||||
return b.Finish()
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ type serveHTTPContext struct {
|
||||
//
|
||||
// This is not used in userspace-networking mode.
|
||||
//
|
||||
// localListener is used by tailscale serve (TCP only) as well as the built-in web client.
|
||||
// localListener is used by tailscale serve (TCP only), the built-in web client and tailfs.
|
||||
// Most serve traffic and peer traffic for the web client are intercepted by netstack.
|
||||
// This listener exists purely for connections from the machine itself, as that goes via the kernel,
|
||||
// so we need to be in the kernel's listening/routing tables.
|
||||
|
||||
@@ -5,6 +5,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
@@ -29,7 +30,6 @@ import (
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -157,7 +157,7 @@ func TestGetServeHandler(t *testing.T) {
|
||||
},
|
||||
TLS: &tls.ConnectionState{ServerName: serverName},
|
||||
}
|
||||
port := cmpx.Or(tt.port, 443)
|
||||
port := cmp.Or(tt.port, 443)
|
||||
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(), &serveHTTPContext{
|
||||
DestPort: port,
|
||||
}))
|
||||
|
||||
@@ -486,7 +486,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName(), qt.Equals, "user1")
|
||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user1")
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, b.State())
|
||||
}
|
||||
@@ -675,7 +675,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[1].Prefs.Persist(), qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName(), qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
@@ -816,7 +816,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName(), qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
|
||||
280
ipn/ipnlocal/tailfs.go
Normal file
280
ipn/ipnlocal/tailfs.go
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
const (
|
||||
// TailFSLocalPort is the port on which the TailFS listens for location
|
||||
// connections on quad 100.
|
||||
TailFSLocalPort = 8080
|
||||
|
||||
tailfsSharesStateKey = ipn.StateKey("_tailfs-shares")
|
||||
)
|
||||
|
||||
var (
|
||||
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
|
||||
errInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
|
||||
)
|
||||
|
||||
// TailFSSharingEnabled reports whether sharing to remote nodes via tailfs is
|
||||
// enabled. This is currently based on checking for the tailfs:share node
|
||||
// attribute.
|
||||
func (b *LocalBackend) TailFSSharingEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.tailFSSharingEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSSharingEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailFSShare)
|
||||
}
|
||||
|
||||
// TailFSAccessEnabled reports whether accessing TailFS shares on remote nodes
|
||||
// is enabled. This is currently based on checking for the tailfs:access node
|
||||
// attribute.
|
||||
func (b *LocalBackend) TailFSAccessEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.tailFSAccessEnabledLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSAccessEnabledLocked() bool {
|
||||
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailFSAccess)
|
||||
}
|
||||
|
||||
// TailFSSetFileServerAddr tells tailfs to use the given address for connecting
|
||||
// to the tailfs.FileServer that's exposing local files as an unprivileged
|
||||
// user.
|
||||
func (b *LocalBackend) TailFSSetFileServerAddr(addr string) error {
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
if !ok {
|
||||
return errors.New("tailfs not enabled")
|
||||
}
|
||||
|
||||
fs.SetFileServerAddr(addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TailFSAddShare adds the given share if no share with that name exists, or
|
||||
// replaces the existing share if one with the same name already exists.
|
||||
// To avoid potential incompatibilities across file systems, share names are
|
||||
// limited to alphanumeric characters and the underscore _.
|
||||
func (b *LocalBackend) TailFSAddShare(share *tailfs.Share) error {
|
||||
var err error
|
||||
share.Name, err = normalizeShareName(share.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.tailfsAddShareLocked(share)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.tailfsNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeShareName normalizes the given share name and returns an error if
|
||||
// it contains any disallowed characters.
|
||||
func normalizeShareName(name string) (string, error) {
|
||||
// Force all share names to lowercase to avoid potential incompatibilities
|
||||
// with clients that don't support case-sensitive filenames.
|
||||
name = strings.ToLower(name)
|
||||
|
||||
// Trim whitespace
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
if !shareNameRegex.MatchString(name) {
|
||||
return "", errInvalidShareName
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]string, error) {
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
if !ok {
|
||||
return nil, errors.New("tailfs not enabled")
|
||||
}
|
||||
|
||||
shares, err := b.tailFSGetSharesLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares[share.Name] = share
|
||||
data, err := json.Marshal(shares)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
err = b.store.WriteState(tailfsSharesStateKey, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write state: %w", err)
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return shareNameMap(shares), nil
|
||||
}
|
||||
|
||||
// TailFSRemoveShare removes the named share. Share names are forced to
|
||||
// lowercase.
|
||||
func (b *LocalBackend) TailFSRemoveShare(name string) error {
|
||||
// Force all share names to lowercase to avoid potential incompatibilities
|
||||
// with clients that don't support case-sensitive filenames.
|
||||
var err error
|
||||
name, err = normalizeShareName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
shares, err := b.tailfsRemoveShareLocked(name)
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.tailfsNotifyShares(shares)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string, error) {
|
||||
fs, ok := b.sys.TailFSForRemote.GetOK()
|
||||
if !ok {
|
||||
return nil, errors.New("tailfs not enabled")
|
||||
}
|
||||
|
||||
shares, err := b.tailFSGetSharesLocked()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, shareExists := shares[name]
|
||||
if !shareExists {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
delete(shares, name)
|
||||
data, err := json.Marshal(shares)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
err = b.store.WriteState(tailfsSharesStateKey, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write state: %w", err)
|
||||
}
|
||||
fs.SetShares(shares)
|
||||
|
||||
return shareNameMap(shares), nil
|
||||
}
|
||||
|
||||
func shareNameMap(sharesByName map[string]*tailfs.Share) map[string]string {
|
||||
sharesMap := make(map[string]string, len(sharesByName))
|
||||
for _, share := range sharesByName {
|
||||
sharesMap[share.Name] = share.Path
|
||||
}
|
||||
return sharesMap
|
||||
}
|
||||
|
||||
// tailfsNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
|
||||
// about the latest set of shares, supplied as a map of name -> directory.
|
||||
func (b *LocalBackend) tailfsNotifyShares(shares map[string]string) {
|
||||
b.send(ipn.Notify{TailFSShares: shares})
|
||||
}
|
||||
|
||||
// tailFSNotifyCurrentSharesLocked sends an ipn.Notify with the current set of
|
||||
// TailFS shares.
|
||||
func (b *LocalBackend) tailFSNotifyCurrentSharesLocked() {
|
||||
shares, err := b.tailFSGetSharesLocked()
|
||||
if err != nil {
|
||||
b.logf("error notifying current tailfs shares: %v", err)
|
||||
return
|
||||
}
|
||||
// Do the below on a goroutine to avoid deadlocking on b.mu in b.send().
|
||||
go b.tailfsNotifyShares(shareNameMap(shares))
|
||||
}
|
||||
|
||||
// TailFSGetShares returns the current set of shares from the state store,
|
||||
// stored under ipn.StateKey("_tailfs-shares").
|
||||
func (b *LocalBackend) TailFSGetShares() (map[string]*tailfs.Share, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.tailFSGetSharesLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tailFSGetSharesLocked() (map[string]*tailfs.Share, error) {
|
||||
data, err := b.store.ReadState(tailfsSharesStateKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, ipn.ErrStateNotExist) {
|
||||
return make(map[string]*tailfs.Share), nil
|
||||
}
|
||||
return nil, fmt.Errorf("read state: %w", err)
|
||||
}
|
||||
|
||||
var shares map[string]*tailfs.Share
|
||||
err = json.Unmarshal(data, &shares)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
// updateTailFSPeersLocked sets all applicable peers from the netmap as tailfs
|
||||
// remotes.
|
||||
func (b *LocalBackend) updateTailFSPeersLocked(nm *netmap.NetworkMap) {
|
||||
fs, ok := b.sys.TailFSForLocal.GetOK()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
tailfsRemotes := make([]*tailfs.Remote, 0, len(nm.Peers))
|
||||
for _, p := range nm.Peers {
|
||||
peerID := p.ID()
|
||||
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailFSPrefix[1:])
|
||||
tailfsRemotes = append(tailfsRemotes, &tailfs.Remote{
|
||||
Name: p.DisplayName(false),
|
||||
URL: url,
|
||||
Available: func() bool {
|
||||
// TODO(oxtoacart): need to figure out a performant and reliable way to only
|
||||
// show the peers that have shares to which we have access
|
||||
// This will require work on the control server to transmit the inverse
|
||||
// of the "tailscale.com/cap/tailfs" capability.
|
||||
// For now, at least limit it only to nodes that are online.
|
||||
// Note, we have to iterate the latest netmap because the peer we got from the first iteration may not be it
|
||||
b.mu.Lock()
|
||||
latestNetMap := b.netMap
|
||||
b.mu.Unlock()
|
||||
|
||||
for _, candidate := range latestNetMap.Peers {
|
||||
if candidate.ID() == peerID {
|
||||
online := candidate.Online()
|
||||
// TODO(oxtoacart): for some reason, this correctly
|
||||
// catches when a node goes from offline to online,
|
||||
// but not the other way around...
|
||||
return online != nil && *online
|
||||
}
|
||||
}
|
||||
|
||||
// peer not found, must not be available
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailFSTransport{b: b})
|
||||
}
|
||||
40
ipn/ipnlocal/tailfs_test.go
Normal file
40
ipn/ipnlocal/tailfs_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeShareName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: " (_this is A 5 nAme )_ ",
|
||||
want: "(_this is a 5 name )_",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
err: errInvalidShareName,
|
||||
},
|
||||
{
|
||||
name: "generally good except for .",
|
||||
err: errInvalidShareName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("name %q", tt.name), func(t *testing.T) {
|
||||
got, err := normalizeShareName(tt.name)
|
||||
if tt.err != nil && err != tt.err {
|
||||
t.Errorf("wanted error %v, got %v", tt.err, err)
|
||||
} else if got != tt.want {
|
||||
t.Errorf("wanted %q, got %q", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -41,6 +43,7 @@ import (
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -107,6 +110,8 @@ var handler = map[string]localAPIHandler{
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"tailfs/fileserver-address": (*Handler).serveTailFSFileServerAddr,
|
||||
"tailfs/shares": (*Handler).serveShares,
|
||||
"start": (*Handler).serveStart,
|
||||
"status": (*Handler).serveStatus,
|
||||
"tka/init": (*Handler).serveTKAInit,
|
||||
@@ -1107,7 +1112,7 @@ func (h *Handler) connIsLocalAdmin() bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Short timeout just in case sudo hands for some reason.
|
||||
// Short timeout just in case sudo hangs for some reason.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
|
||||
@@ -1120,6 +1125,34 @@ func (h *Handler) connIsLocalAdmin() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getUsername() (string, error) {
|
||||
if h.ConnIdentity == nil {
|
||||
h.logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
|
||||
return "", errors.New("missing ConnIdentity")
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
tok, err := h.ConnIdentity.WindowsToken()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get windows token: %w", err)
|
||||
}
|
||||
defer tok.Close()
|
||||
return tok.Username()
|
||||
case "darwin", "linux":
|
||||
uid, ok := h.ConnIdentity.Creds().UserID()
|
||||
if !ok {
|
||||
return "", errors.New("missing user ID")
|
||||
}
|
||||
u, err := osuser.LookupByUID(uid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup user: %w", err)
|
||||
}
|
||||
return u.Username, nil
|
||||
default:
|
||||
return "", errors.New("unsupported OS")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
||||
@@ -1991,7 +2024,7 @@ func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(wrappedKey))
|
||||
}
|
||||
|
||||
@@ -2045,7 +2078,7 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2069,7 +2102,7 @@ func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2391,7 +2424,7 @@ func (h *Handler) serveDebugCapture(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.(http.Flusher).Flush()
|
||||
h.b.StreamDebugCapture(r.Context(), w)
|
||||
}
|
||||
@@ -2498,6 +2531,95 @@ func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(ups)
|
||||
}
|
||||
|
||||
// serveTailFSFileServerAddr handles updates of the tailfs file server address.
|
||||
func (h *Handler) serveTailFSFileServerAddr(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.b.TailFSSetFileServerAddr(string(b))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// serveShares handles the management of tailfs shares.
|
||||
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.b.TailFSSharingEnabled() {
|
||||
http.Error(w, `tailfs sharing not enabled, please add the attribute "tailfs:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
var share tailfs.Share
|
||||
err := json.NewDecoder(r.Body).Decode(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
share.Path = path.Clean(share.Path)
|
||||
fi, err := os.Stat(share.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
http.Error(w, "not a directory", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if tailfs.AllowShareAs() {
|
||||
// share as the connected user
|
||||
username, err := h.getUsername()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
share.As = username
|
||||
}
|
||||
err = h.b.TailFSAddShare(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
case "DELETE":
|
||||
var share tailfs.Share
|
||||
err := json.NewDecoder(r.Body).Decode(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.b.TailFSRemoveShare(share.Name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "share not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "GET":
|
||||
shares, err := h.b.TailFSGetShares()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(shares)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"NetfilterKind",
|
||||
"Persist",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
if have := fieldsOf(reflect.TypeFor[Prefs]()); !reflect.DeepEqual(have, prefsHandles) {
|
||||
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, prefsHandles)
|
||||
}
|
||||
@@ -615,14 +615,14 @@ func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
||||
|
||||
func TestMaskedPrefsFields(t *testing.T) {
|
||||
have := map[string]bool{}
|
||||
for _, f := range fieldsOf(reflect.TypeOf(Prefs{})) {
|
||||
for _, f := range fieldsOf(reflect.TypeFor[Prefs]()) {
|
||||
if f == "Persist" {
|
||||
// This one can't be edited.
|
||||
continue
|
||||
}
|
||||
have[f] = true
|
||||
}
|
||||
for _, f := range fieldsOf(reflect.TypeOf(MaskedPrefs{})) {
|
||||
for _, f := range fieldsOf(reflect.TypeFor[MaskedPrefs]()) {
|
||||
if f == "Prefs" {
|
||||
continue
|
||||
}
|
||||
@@ -644,8 +644,8 @@ func TestMaskedPrefsFields(t *testing.T) {
|
||||
|
||||
// And also make sure they line up in the right order, which
|
||||
// ApplyEdits assumes.
|
||||
pt := reflect.TypeOf(Prefs{})
|
||||
mt := reflect.TypeOf(MaskedPrefs{})
|
||||
pt := reflect.TypeFor[Prefs]()
|
||||
mt := reflect.TypeFor[MaskedPrefs]()
|
||||
for i := 0; i < mt.NumField(); i++ {
|
||||
name := mt.Field(i).Name
|
||||
if i == 0 {
|
||||
|
||||
@@ -49,7 +49,7 @@ func init() {
|
||||
|
||||
// Adds the list of known types to api.Scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{})
|
||||
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{}, &ProxyClass{}, &ProxyClassList{})
|
||||
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
@@ -68,6 +68,12 @@ type ConnectorSpec struct {
|
||||
// and 63 characters long.
|
||||
// +optional
|
||||
Hostname Hostname `json:"hostname,omitempty"`
|
||||
// ProxyClass is the name of the ProxyClass custom resource that
|
||||
// contains configuration options that should be applied to the
|
||||
// resources created for this Connector. If unset, the operator will
|
||||
// create resources with the default configuration.
|
||||
// +optional
|
||||
ProxyClass string `json:"proxyClass,omitempty"`
|
||||
// SubnetRouter defines subnet routes that the Connector node should
|
||||
// expose to tailnet. If unset, none are exposed.
|
||||
// https://tailscale.com/kb/1019/subnets/
|
||||
@@ -180,5 +186,6 @@ type ConnectorCondition struct {
|
||||
type ConnectorConditionType string
|
||||
|
||||
const (
|
||||
ConnectorReady ConnectorConditionType = `ConnectorReady`
|
||||
ConnectorReady ConnectorConditionType = `ConnectorReady`
|
||||
ProxyClassready ConnectorConditionType = `ProxyClassReady`
|
||||
)
|
||||
|
||||
148
k8s-operator/apis/v1alpha1/types_proxyclass.go
Normal file
148
k8s-operator/apis/v1alpha1/types_proxyclass.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var ProxyClassKind = "ProxyClass"
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type == "ProxyClassReady")].reason`,description="Status of the ProxyClass."
|
||||
|
||||
type ProxyClass struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ProxyClassSpec `json:"spec"`
|
||||
|
||||
// +optional
|
||||
Status ProxyClassStatus `json:"status"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type ProxyClassList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
|
||||
Items []ProxyClass `json:"items"`
|
||||
}
|
||||
|
||||
type ProxyClassSpec struct {
|
||||
// Proxy's StatefulSet spec.
|
||||
StatefulSet *StatefulSet `json:"statefulSet"`
|
||||
}
|
||||
|
||||
type StatefulSet struct {
|
||||
// Labels that will be added to the StatefulSet created for the proxy.
|
||||
// Any labels specified here will be merged with the default labels
|
||||
// applied to the StatefulSet by the Tailscale Kubernetes operator as
|
||||
// well as any other labels that might have been applied by other
|
||||
// actors.
|
||||
// Label keys and values must be valid Kubernetes label keys and values.
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Annotations that will be added to the StatefulSet created for the proxy.
|
||||
// Any Annotations specified here will be merged with the default annotations
|
||||
// applied to the StatefulSet by the Tailscale Kubernetes operator as
|
||||
// well as any other annotations that might have been applied by other
|
||||
// actors.
|
||||
// Annotations must be valid Kubernetes annotations.
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// Configuration for the proxy Pod.
|
||||
// +optional
|
||||
Pod *Pod `json:"pod,omitempty"`
|
||||
// In future: allow users to tell the operator that spec.replicas for a
|
||||
// statefulset should be unset to allow HPA manage this field (i.e if
|
||||
// users set it to 0 here, unset the replicas field).
|
||||
// +optional
|
||||
Replicas *int32 `json:"replicas,omitempty"`
|
||||
}
|
||||
|
||||
type Pod struct {
|
||||
// Labels that will be added to the proxy Pod.
|
||||
// Any labels specified here will be merged with the default labels
|
||||
// applied to the Pod by the Tailscale Kubernetes operator.
|
||||
// Label keys and values must be valid Kubernetes label keys and values.
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Annotations that will be added to the proxy Pod.
|
||||
// Any annotations specified here will be merged with the default
|
||||
// annotations applied to the Pod by the Tailscale Kubernetes operator.
|
||||
// Annotations must be valid Kubernetes annotations.
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// Configuration for the proxy container running tailscale.
|
||||
// +optional
|
||||
TailscaleContainer *Container `json:"tailscaleContainer,omitempty"`
|
||||
// Configuration for the proxy init container that enables forwarding.
|
||||
// +optional
|
||||
TailscaleInitContainer *Container `json:"tailscaleInitContainer,omitempty"`
|
||||
// Proxy Pod's security context.
|
||||
// By default Tailscale Kubernetes operator does not apply any Pod
|
||||
// security context.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-2
|
||||
// +optional
|
||||
SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"`
|
||||
// Proxy Pod's image pull Secrets.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec
|
||||
// +optional
|
||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||
// Proxy Pod's node name.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
// +optional
|
||||
NodeName string `json:"nodeName,omitempty"`
|
||||
// Proxy Pod's node selector.
|
||||
// By default Tailscale Kubernetes operator does not apply any node
|
||||
// selector.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
// +optional
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
// Proxy Pod's tolerations.
|
||||
// By default Tailscale Kubernetes operator does not apply any
|
||||
// tolerations.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
|
||||
// +optional
|
||||
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
// Container security context.
|
||||
// Security context specified here will override the security context by the operator.
|
||||
// By default the operator:
|
||||
// - sets 'privileged: true' for the init container
|
||||
// - set NET_ADMIN capability for tailscale container for proxies that
|
||||
// are created for Services or Connector.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context
|
||||
// +optional
|
||||
SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"`
|
||||
// Container resource requirements.
|
||||
// By default Tailscale Kubernetes operator does not apply any resource
|
||||
// requirements. The amount of resources required wil depend on the
|
||||
// amount of resources the operator needs to parse, usage patterns and
|
||||
// cluster size.
|
||||
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
|
||||
// +optional
|
||||
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
type ProxyClassStatus struct {
|
||||
// List of status conditions to indicate the status of the ProxyClass.
|
||||
// Known condition types are `ProxyClassReady`.
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
Conditions []ConnectorCondition `json:"conditions,omitempty"`
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -136,6 +137,191 @@ func (in *ConnectorStatus) DeepCopy() *ConnectorStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Container) DeepCopyInto(out *Container) {
|
||||
*out = *in
|
||||
if in.SecurityContext != nil {
|
||||
in, out := &in.SecurityContext, &out.SecurityContext
|
||||
*out = new(v1.SecurityContext)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.Resources.DeepCopyInto(&out.Resources)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container.
|
||||
func (in *Container) DeepCopy() *Container {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Container)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Pod) DeepCopyInto(out *Pod) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.TailscaleContainer != nil {
|
||||
in, out := &in.TailscaleContainer, &out.TailscaleContainer
|
||||
*out = new(Container)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.TailscaleInitContainer != nil {
|
||||
in, out := &in.TailscaleInitContainer, &out.TailscaleInitContainer
|
||||
*out = new(Container)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.SecurityContext != nil {
|
||||
in, out := &in.SecurityContext, &out.SecurityContext
|
||||
*out = new(v1.PodSecurityContext)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ImagePullSecrets != nil {
|
||||
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||
*out = make([]v1.LocalObjectReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.NodeSelector != nil {
|
||||
in, out := &in.NodeSelector, &out.NodeSelector
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Tolerations != nil {
|
||||
in, out := &in.Tolerations, &out.Tolerations
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pod.
|
||||
func (in *Pod) DeepCopy() *Pod {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Pod)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ProxyClass) DeepCopyInto(out *ProxyClass) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClass.
|
||||
func (in *ProxyClass) DeepCopy() *ProxyClass {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ProxyClass)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ProxyClass) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ProxyClassList) DeepCopyInto(out *ProxyClassList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ProxyClass, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClassList.
|
||||
func (in *ProxyClassList) DeepCopy() *ProxyClassList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ProxyClassList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ProxyClassList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ProxyClassSpec) DeepCopyInto(out *ProxyClassSpec) {
|
||||
*out = *in
|
||||
if in.StatefulSet != nil {
|
||||
in, out := &in.StatefulSet, &out.StatefulSet
|
||||
*out = new(StatefulSet)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClassSpec.
|
||||
func (in *ProxyClassSpec) DeepCopy() *ProxyClassSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ProxyClassSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ProxyClassStatus) DeepCopyInto(out *ProxyClassStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]ConnectorCondition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClassStatus.
|
||||
func (in *ProxyClassStatus) DeepCopy() *ProxyClassStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ProxyClassStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in Routes) DeepCopyInto(out *Routes) {
|
||||
{
|
||||
@@ -155,6 +341,45 @@ func (in Routes) DeepCopy() Routes {
|
||||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *StatefulSet) DeepCopyInto(out *StatefulSet) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Pod != nil {
|
||||
in, out := &in.Pod, &out.Pod
|
||||
*out = new(Pod)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Replicas != nil {
|
||||
in, out := &in.Replicas, &out.Replicas
|
||||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatefulSet.
|
||||
func (in *StatefulSet) DeepCopy() *StatefulSet {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(StatefulSet)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SubnetRouter) DeepCopyInto(out *SubnetRouter) {
|
||||
*out = *in
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user