Update tailscale to v1.102.1

This commit is contained in:
世界
2026-08-30 17:41:45 +08:00
parent 9bf39f85f9
commit e797c2d0ba
18 changed files with 282 additions and 210 deletions
+2 -1
View File
@@ -24,7 +24,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/net/http2/h2c" //nolint:staticcheck
)
func RegisterRealmService(registry *boxService.Registry) {
@@ -98,6 +98,7 @@ func NewRealmService(ctx context.Context, logger log.ContextLogger, tag string,
Listen: options.ListenOptions,
}),
httpServer: &http.Server{
//nolint:staticcheck
Handler: h2c.NewHandler(chiRouter, &http2.Server{
IdleTimeout: time.Duration(options.IdleTimeout),
ReadIdleTimeout: time.Duration(options.KeepAlivePeriod),
+2 -1
View File
@@ -26,7 +26,7 @@ import (
sHttp "github.com/sagernet/sing/protocol/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/net/http2/h2c" //nolint:staticcheck
)
var (
@@ -102,6 +102,7 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
return err
}
n.httpServer = &http.Server{
//nolint:staticcheck
Handler: h2c.NewHandler(n, &http2.Server{}),
BaseContext: func(listener net.Listener) context.Context {
return n.ctx
+41 -18
View File
@@ -27,6 +27,7 @@ import (
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
nDNS "github.com/sagernet/tailscale/net/dns"
nDNSResolver "github.com/sagernet/tailscale/net/dns/resolver"
"github.com/sagernet/tailscale/types/dnstype"
"github.com/sagernet/tailscale/util/dnsname"
"github.com/sagernet/tailscale/wgengine/router"
@@ -55,6 +56,7 @@ type DNSTransport struct {
routePrefixes []netip.Prefix
routes map[string][]adapter.DNSTransport
hosts map[string][]netip.Addr
magicHosts nDNSResolver.MagicDNSHosts
searchDomains []string
defaultResolvers []adapter.DNSTransport
}
@@ -150,6 +152,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
t.routePrefixes = routePrefixes
t.routes = routes
t.hosts = hosts
t.magicHosts = t.endpoint.server.ExportLocalBackend().ExportMagicDNSHosts()
t.searchDomains = searchDomains
t.defaultResolvers = defaultResolvers
t.access.Unlock()
@@ -241,6 +244,7 @@ func (t *DNSTransport) Close() error {
t.routePrefixes = nil
t.routes = nil
t.hosts = nil
t.magicHosts = nil
t.defaultResolvers = nil
t.access.Unlock()
@@ -261,9 +265,14 @@ func (t *DNSTransport) Raw() bool {
func (t *DNSTransport) PreferredDomain(domain string) bool {
t.access.RLock()
hosts := t.hosts
magicHosts := t.magicHosts
routes := t.routes
searchDomains := t.searchDomains
t.access.RUnlock()
if _, loaded := hosts[domain]; loaded {
if _, loaded := lookupHosts(hosts, magicHosts, domain); loaded {
return true
}
if t.acceptSearchDomain && len(searchDomains) > 0 && mDNS.CountLabel(domain) == 1 {
return true
}
for suffix := range routes {
@@ -351,30 +360,20 @@ func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allo
t.access.RLock()
hosts := t.hosts
magicHosts := t.magicHosts
routes := t.routes
defaultResolvers := t.defaultResolvers
t.access.RUnlock()
addresses, hostsLoaded := hosts[question.Name]
addresses, hostsLoaded := lookupHosts(hosts, magicHosts, question.Name)
if hostsLoaded {
switch question.Qtype {
case mDNS.TypeA:
addresses4 := common.Filter(addresses, func(addr netip.Addr) bool {
return addr.Is4()
})
if len(addresses4) > 0 {
callback(dns.FixedResponse(message.Id, question, addresses4, C.DefaultDNSTTL), nil)
return
}
case mDNS.TypeAAAA:
addresses6 := common.Filter(addresses, func(addr netip.Addr) bool {
return addr.Is6()
})
if len(addresses6) > 0 {
callback(dns.FixedResponse(message.Id, question, addresses6, C.DefaultDNSTTL), nil)
return
}
case mDNS.TypeA, mDNS.TypeAAAA:
callback(dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil)
default:
callback(dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil)
}
return
}
for domainSuffix, transports := range routes {
if mDNS.IsSubDomain(domainSuffix, question.Name) {
@@ -404,6 +403,30 @@ func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allo
callback(nil, dns.RcodeNameError)
}
func lookupHosts(hosts map[string][]netip.Addr, magicHosts nDNSResolver.MagicDNSHosts, name string) ([]netip.Addr, bool) {
addresses, loaded := hosts[name]
if loaded {
return addresses, true
}
if magicHosts == nil {
return nil, false
}
fqdn, err := dnsname.ToFQDN(name)
if err != nil {
return nil, false
}
addresses, loaded = magicHosts.LookupHost(fqdn)
if loaded {
return addresses, true
}
for parent := fqdn.Parent(); parent != ""; parent = parent.Parent() {
if magicHosts.SubdomainHost(parent) {
return magicHosts.LookupHost(parent)
}
}
return nil, false
}
func resolverExchangers(resolvers []adapter.DNSTransport, message *mDNS.Msg) []transport.AsyncExchanger {
return common.Map(resolvers, func(resolver adapter.DNSTransport) transport.AsyncExchanger {
return func(ctx context.Context, callback func(response *mDNS.Msg, err error)) {
+79 -59
View File
@@ -57,12 +57,10 @@ import (
"github.com/sagernet/tailscale/types/nettype"
"github.com/sagernet/tailscale/version"
"github.com/sagernet/tailscale/wgengine"
"github.com/sagernet/tailscale/wgengine/filter"
"github.com/sagernet/tailscale/wgengine/router"
"github.com/sagernet/tailscale/wgengine/wgcfg"
mDNS "github.com/miekg/dns"
"go4.org/netipx"
)
var (
@@ -92,7 +90,6 @@ type Endpoint struct {
server *tsnet.Server
stack *stack.Stack
icmpForwarder *tun.ICMPForwarder
filter *atomic.Pointer[filter.Filter]
returnAccess sync.Mutex
returnPath tun.Return
wgEngine wgengine.ExportedUserspaceEngine
@@ -105,7 +102,6 @@ type Endpoint struct {
routeDomains common.TypedValue[map[string]bool]
routeSuffixes common.TypedValue[[]string]
searchDomains atomic.Bool
routePrefixes atomic.Pointer[netipx.IPSet]
acceptRoutes bool
exitNode string
@@ -330,7 +326,7 @@ func (t *Endpoint) start() error {
}
t.systemTun = systemTun
t.systemDialer = systemDialer
t.server.TunDevice = wgTunDevice
t.server.Tun = wgTunDevice
}
if t.network.AutoRedirectOutputMark() != 0 {
netns.SetControlFunc(t.network.AutoRedirectOutputMarkFunc())
@@ -467,12 +463,10 @@ func (t *Endpoint) postStart() error {
t.logger.Warn("SSH server degraded: ", degraded)
}
}
localBackend := t.server.ExportLocalBackend()
err = t.editPrefs(sshEnabled)
if err != nil {
return err
}
t.filter = localBackend.ExportFilter()
if sshEnabled {
sshServer, err := tailssh.New(t.ctx, t.server, t.platformInterface, t.sshServerOptions, t.logger)
if err != nil {
@@ -494,48 +488,76 @@ func (t *Endpoint) watchState() {
localBackend := t.server.ExportLocalBackend()
var reportedAuthURL string
exitNodePending := t.exitNode != ""
localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
if roNotify.State == nil && roNotify.BrowseToURL == nil {
return true
running := false
tryApplyExitNode := func() {
err := t.applyExitNode()
if err != nil {
t.logger.Error("set exit node: ", err)
} else {
exitNodePending = false
}
status := localBackend.StatusWithoutPeers()
switch status.BackendState {
case ipn.NoState.String(), ipn.NeedsLogin.String():
if t.exitNode != "" {
exitNodePending = true
}
for {
var busError string
localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState|ipn.NotifyPeerPatches, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
if roNotify.ErrMessage != nil {
busError = *roNotify.ErrMessage
return false
}
authURL := status.AuthURL
if authURL == "" || authURL == reportedAuthURL {
if running && exitNodePending && len(roNotify.PeersChanged) > 0 {
tryApplyExitNode()
}
if roNotify.State == nil && roNotify.BrowseToURL == nil {
return true
}
reportedAuthURL = authURL
t.logger.Info("Waiting for authentication: ", authURL)
if t.platformInterface != nil && t.platformInterface.UsePlatformNotification() {
err := t.platformInterface.SendNotification(&adapter.Notification{
Identifier: "tailscale-authentication",
TypeName: "Tailscale Authentication Notifications",
TypeID: 10,
Title: "Tailscale Authentication",
Body: F.ToString("Tailscale outbound[", t.Tag(), "] is waiting for authentication."),
OpenURL: authURL,
})
if err != nil {
t.logger.Error("send authentication notification: ", err)
}
}
case ipn.Running.String():
reportedAuthURL = ""
if exitNodePending {
err := t.applyExitNode()
if err != nil {
t.logger.Error("set exit node: ", err)
} else {
exitNodePending = false
status := localBackend.StatusWithoutPeers()
running = status.BackendState == ipn.Running.String()
switch status.BackendState {
case ipn.NoState.String(), ipn.NeedsLogin.String():
if t.exitNode != "" {
exitNodePending = true
}
authURL := status.AuthURL
if authURL == "" || authURL == reportedAuthURL {
return true
}
reportedAuthURL = authURL
t.logger.Info("Waiting for authentication: ", authURL)
if t.platformInterface != nil && t.platformInterface.UsePlatformNotification() {
err := t.platformInterface.SendNotification(&adapter.Notification{
Identifier: "tailscale-authentication",
TypeName: "Tailscale Authentication Notifications",
TypeID: 10,
Title: "Tailscale Authentication",
Body: F.ToString("Tailscale outbound[", t.Tag(), "] is waiting for authentication."),
OpenURL: authURL,
})
if err != nil {
t.logger.Error("send authentication notification: ", err)
}
}
case ipn.Running.String():
reportedAuthURL = ""
if exitNodePending {
tryApplyExitNode()
}
}
return true
})
if t.ctx.Err() != nil {
return
}
return true
})
if busError != "" {
t.logger.Warn("restarting state watcher: ", busError)
} else {
t.logger.Warn("state watcher stopped unexpectedly, restarting")
}
select {
case <-t.ctx.Done():
return
case <-time.After(time.Second):
}
}
}
func (t *Endpoint) editPrefs(sshEnabled bool) error {
@@ -895,6 +917,12 @@ func (t *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain stri
if routeDomains[domain] {
return true
}
if t.started.Load() {
magicHosts := t.server.ExportLocalBackend().ExportMagicDNSHosts()
if _, found := lookupHosts(nil, magicHosts, domain); found {
return true
}
}
for _, suffix := range t.routeSuffixes.Load() {
if mDNS.IsSubDomain(suffix, domain) {
return true
@@ -904,11 +932,11 @@ func (t *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain stri
}
func (t *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
routePrefixes := t.routePrefixes.Load()
if routePrefixes == nil {
if !t.started.Load() {
return false
}
return routePrefixes.Contains(address)
peer, found := t.server.ExportLocalBackend().PeerForIP(address)
return found && !peer.IsSelf && peer.Route.Bits() > 0
}
func (t *Endpoint) Server() *tsnet.Server {
@@ -919,6 +947,12 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf
if cfg == nil || dnsCfg == nil {
return
}
// The engine invokes the listener on every Reconfig call, including
// unchanged ones: SSH policy lives only in the netmap, outside the
// three configs, so the SSH hook must run before the change check.
if t.sshReconfigHook != nil {
t.sshReconfigHook(cfg, routerCfg, dnsCfg)
}
if t.cfg != nil && reflect.DeepEqual(t.cfg, cfg) &&
t.routerCfg != nil && reflect.DeepEqual(t.routerCfg, routerCfg) &&
t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg) {
@@ -943,23 +977,9 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf
t.routeSuffixes.Store(routeSuffixes)
t.searchDomains.Store(len(dnsCfg.SearchDomains) > 0)
var builder netipx.IPSetBuilder
for _, peer := range cfg.Peers {
for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Bits() == 0 {
continue
}
builder.AddPrefix(allowedIP)
}
}
t.routePrefixes.Store(common.Must1(builder.IPSet()))
if t.onReconfigHook != nil {
t.onReconfigHook(cfg, routerCfg, dnsCfg)
}
if t.sshReconfigHook != nil {
t.sshReconfigHook(cfg, routerCfg, dnsCfg)
}
}
func addressFromAddr(destination netip.Addr) tcpip.Address {
+2 -2
View File
@@ -41,8 +41,8 @@ func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination n
if destination.Addr() == inet4Address || destination.Addr() == inet6Address {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
if t.filter != nil {
tsFilter := t.filter.Load()
if t.started.Load() {
tsFilter := t.wgEngine.GetFilter()
if tsFilter != nil {
var (
ipProto ipproto.Proto
+71 -33
View File
@@ -5,6 +5,7 @@ package tailscale
import (
"context"
"slices"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/tailscale/ipn"
@@ -15,25 +16,60 @@ var _ adapter.TailscaleEndpoint = (*Endpoint)(nil)
func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapter.TailscaleEndpointStatus)) error {
localBackend := t.server.ExportLocalBackend()
sendStatus := func() {
status := localBackend.Status()
result := convertTailscaleStatus(status)
result.KeyAuth = t.keyAuth
fn(result)
}
sendStatus()
localBackend.WatchNotifications(ctx, ipn.NotifyInitialState|ipn.NotifyInitialNetMap|ipn.NotifyRateLimit, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
// The notification callback must stay cheap and non-blocking: a
// watcher whose queue fills is disconnected by the IPN bus, so
// status collection and delivery (which blocks on the subscriber)
// run on a separate coalescing goroutine.
updateSignal := make(chan struct{}, 1)
scheduleUpdate := func() {
select {
case <-ctx.Done():
return false
case updateSignal <- struct{}{}:
default:
}
if roNotify.State != nil || roNotify.NetMap != nil || roNotify.BrowseToURL != nil || roNotify.Prefs != nil {
sendStatus()
}
go func() {
for {
select {
case <-ctx.Done():
return
case <-updateSignal:
}
status := localBackend.Status()
result := convertTailscaleStatus(status)
result.KeyAuth = t.keyAuth
fn(result)
}
return true
})
return ctx.Err()
}()
scheduleUpdate()
for {
var busError string
localBackend.WatchNotifications(ctx, ipn.NotifyInitialState|ipn.NotifyPeerPatches, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
if roNotify.ErrMessage != nil {
busError = *roNotify.ErrMessage
return false
}
if roNotify.State != nil || roNotify.SelfChange != nil ||
len(roNotify.PeersChanged) > 0 || len(roNotify.PeersRemoved) > 0 || len(roNotify.PeerChangedPatch) > 0 ||
roNotify.BrowseToURL != nil || roNotify.Prefs != nil {
scheduleUpdate()
}
return true
})
if ctx.Err() != nil {
return ctx.Err()
}
if busError != "" {
t.logger.Warn("restarting status watcher: ", busError)
} else {
t.logger.Warn("status watcher stopped unexpectedly, restarting")
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
}
scheduleUpdate()
}
}
func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointStatus {
@@ -78,25 +114,27 @@ func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointS
return 0
})
}
if status.ExitNodeStatus != nil {
for _, peerKey := range status.Peers() {
peer := status.Peer[peerKey]
if peer.ID == status.ExitNodeStatus.ID {
result.ExitNode = convertTailscalePeer(peer)
break
}
// status.ExitNodeStatus is populated from the cached netmap Peers
// slice, which incremental deltas do not update; the live peer map
// behind status.Peer sets PeerStatus.ExitNode delta-correctly, so it
// is the primary source.
for _, peerKey := range status.Peers() {
peer := status.Peer[peerKey]
if peer.ExitNode {
result.ExitNode = convertTailscalePeer(peer)
break
}
if result.ExitNode == nil {
ips := make([]string, 0, len(status.ExitNodeStatus.TailscaleIPs))
for _, prefix := range status.ExitNodeStatus.TailscaleIPs {
ips = append(ips, prefix.Addr().String())
}
result.ExitNode = &adapter.TailscalePeer{
StableID: string(status.ExitNodeStatus.ID),
TailscaleIPs: ips,
Online: status.ExitNodeStatus.Online,
ExitNode: true,
}
}
if result.ExitNode == nil && status.ExitNodeStatus != nil {
ips := make([]string, 0, len(status.ExitNodeStatus.TailscaleIPs))
for _, prefix := range status.ExitNodeStatus.TailscaleIPs {
ips = append(ips, prefix.Addr().String())
}
result.ExitNode = &adapter.TailscalePeer{
StableID: string(status.ExitNodeStatus.ID),
TailscaleIPs: ips,
Online: status.ExitNodeStatus.Online,
ExitNode: true,
}
}
return result
+4 -4
View File
@@ -287,7 +287,7 @@ func (s *Server) authenticate(ctx gliderssh.Context, conn gossh.ConnMetadata) (*
s.logger.Warn("SSH auth: unknown peer ", remoteAddrPort)
return nil, &gossh.PartialSuccessError{}
}
netMap := localBackend.NetMap()
netMap := localBackend.NetMapNoPeers()
if netMap == nil || netMap.SSHPolicy == nil {
s.logger.Warn("SSH auth: no SSH policy")
return nil, &gossh.PartialSuccessError{}
@@ -419,7 +419,7 @@ func (s *Server) holdAndDelegate(ctx context.Context, action *tailcfg.SSHAction,
srcNodeIP = node.Addresses().At(0).Addr()
}
var dstNodeID string
netMap := lb.NetMap()
netMap := lb.NetMapNoPeers()
if netMap != nil && netMap.SelfNode.Valid() {
dstNodeID = fmt.Sprint(int64(netMap.SelfNode.ID()))
}
@@ -831,7 +831,7 @@ func (s *Server) buildEnvironment(session gliderssh.Session, connInfo *sshConnIn
// capability, matching upstream's capability gate.
acceptEnv := connInfo.acceptEnv
if len(acceptEnv) > 0 {
netMap := s.tsnetServer.ExportLocalBackend().NetMap()
netMap := s.tsnetServer.ExportLocalBackend().NetMapNoPeers()
if netMap == nil || !netMap.HasCap(tailcfg.NodeAttrSSHEnvironmentVariables) {
acceptEnv = nil
}
@@ -957,7 +957,7 @@ func (s *Server) allowReverseUnixForward(ctx gliderssh.Context, socketPath strin
func (s *Server) OnReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *tsDNS.Config) {
localBackend := s.tsnetServer.ExportLocalBackend()
netMap := localBackend.NetMap()
netMap := localBackend.NetMapNoPeers()
if netMap == nil || netMap.SSHPolicy == nil {
return
}