Files
tailscale/logpolicy/logpolicy_test.go
Brad Fitzpatrick 67f8c81e61 logpolicy, net/dnscache: cache log server DNS lookups across dials
logtail's dialer only used dnscache in its fallback path, and built a
new Resolver per dial there, so every new connection to the log server
did a fresh DNS lookup (twice when the first dial failed). On systems
without a caching resolver, that adds up to a lot of queries for
log.tailscale.com.

Share one dnscache.Resolver across all log dialers, and have the
bootstrap path go straight to dnsfallback rather than repeating the
regular lookup. Since the cache TTL is a fixed guess, make
dnscache.Dialer expire a cached entry and look the host up again when
dialing its cached IPs fails, so a moved host isn't stuck on stale IPs.

Updates #15326

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I6a55d72a73ed878faebc15317421a54d9ae293ca
2026-10-01 19:22:39 -07:00

181 lines
3.8 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package logpolicy
import (
"context"
"net/http"
"net/netip"
"os"
"reflect"
"sync/atomic"
"testing"
"tailscale.com/logtail"
"tailscale.com/net/dnscache"
"tailscale.com/net/memnet"
"tailscale.com/tstest"
)
func resetLogTarget() {
os.Unsetenv("TS_LOG_TARGET")
v := reflect.ValueOf(&getLogTargetOnce).Elem()
v.Set(reflect.Zero(v.Type()))
}
func TestLogHost(t *testing.T) {
defer resetLogTarget()
tests := []struct {
env string
want string
}{
{"", logtail.DefaultHost},
{"http://foo.com", "foo.com"},
{"https://foo.com", "foo.com"},
{"https://foo.com/", "foo.com"},
{"https://foo.com:123/", "foo.com"},
}
for _, tt := range tests {
resetLogTarget()
os.Setenv("TS_LOG_TARGET", tt.env)
if got := LogHost(); got != tt.want {
t.Errorf("for env %q, got %q, want %q", tt.env, got, tt.want)
}
}
}
func TestOptions(t *testing.T) {
defer resetLogTarget()
tests := []struct {
name string
opts func() Options
wantBaseURL string
}{
{
name: "default",
opts: func() Options { return Options{} },
wantBaseURL: "",
},
{
name: "custom_baseurl",
opts: func() Options {
os.Setenv("TS_LOG_TARGET", "http://localhost:1234")
return Options{}
},
wantBaseURL: "http://localhost:1234",
},
{
name: "custom_httpc_and_baseurl",
opts: func() Options {
os.Setenv("TS_LOG_TARGET", "http://localhost:12345")
return Options{HTTPC: &http.Client{Transport: noopPretendSuccessTransport{}}}
},
wantBaseURL: "http://localhost:12345",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resetLogTarget()
config, policy := tt.opts().init(false)
if policy == nil {
t.Fatal("unexpected nil policy")
}
if config.BaseURL != tt.wantBaseURL {
t.Errorf("got %q, want %q", config.BaseURL, tt.wantBaseURL)
}
policy.Close()
})
}
}
// TestInvalidLogTarget is a test for #17792
func TestInvalidLogTarget(t *testing.T) {
defer resetLogTarget()
tests := []struct {
name string
logTarget string
}{
{
name: "invalid_url_no_scheme",
logTarget: "not a url at all",
},
{
name: "malformed_url",
logTarget: "ht!tp://invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resetLogTarget()
os.Setenv("TS_LOG_TARGET", tt.logTarget)
opts := Options{
Collection: "test.log.tailscale.io",
Logf: t.Logf,
}
// This should not panic even with invalid log target
config, policy := opts.init(false)
if policy == nil {
t.Fatal("expected non-nil policy")
}
defer policy.Close()
// When log target is invalid, it should fall back to the invalid value
// but not crash. BaseURL should remain empty
if config.BaseURL != "" {
t.Errorf("got BaseURL=%q, want empty", config.BaseURL)
}
})
}
}
// TestDialSharesDNSCache verifies that repeated dials, including from
// separate dial funcs (as used by separate log transports), share one DNS
// lookup.
func TestDialSharesDNSCache(t *testing.T) {
var mn memnet.Network
ln, err := mn.Listen("tcp", "127.0.0.1:443")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
c.Close()
}
}()
var lookups atomic.Int32
tstest.Replace(t, &logDNSCache, &dnscache.Resolver{
UseLastGood: true,
LookupIPForTest: func(ctx context.Context, host string) ([]netip.Addr, error) {
lookups.Add(1)
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
},
})
for range 2 {
dial := makeDialFunc(mn.Dial, nil, t.Logf)
for range 3 {
c, err := dial(t.Context(), "tcp", "log.example.com:443")
if err != nil {
t.Fatal(err)
}
c.Close()
}
}
if got := lookups.Load(); got != 1 {
t.Errorf("got %d DNS lookups; want 1", got)
}
}