mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-10-12 02:08:11 +08:00
477 lines
13 KiB
Go
477 lines
13 KiB
Go
//go:build with_gvisor && !no_tailscale
|
|
|
|
package outbound
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/metacubex/mihomo/component/ca"
|
|
"github.com/metacubex/mihomo/component/dialer"
|
|
"github.com/metacubex/mihomo/component/iface/anet"
|
|
"github.com/metacubex/mihomo/component/resolver"
|
|
C "github.com/metacubex/mihomo/constant"
|
|
"github.com/metacubex/mihomo/dns"
|
|
"github.com/metacubex/mihomo/log"
|
|
|
|
"github.com/metacubex/tailscale/envknob"
|
|
"github.com/metacubex/tailscale/hostinfo"
|
|
"github.com/metacubex/tailscale/ipn"
|
|
"github.com/metacubex/tailscale/net/netmon"
|
|
"github.com/metacubex/tailscale/tailcfg"
|
|
"github.com/metacubex/tailscale/tsnet"
|
|
D "github.com/miekg/dns"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type Tailscale struct {
|
|
*Base
|
|
server *tsnet.Server
|
|
dnsResolver *dns.Resolver
|
|
option TailscaleOption
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
startOnce sync.Once
|
|
startErr error
|
|
|
|
backendInitOnce sync.Once
|
|
backendInitCh chan struct{}
|
|
backendInitErr error
|
|
|
|
serverStarted bool
|
|
|
|
unregisterDNSResolver func()
|
|
}
|
|
|
|
type TailscaleOption struct {
|
|
BasicOption
|
|
Name string `proxy:"name"`
|
|
Hostname string `proxy:"hostname,omitempty"`
|
|
AuthKey string `proxy:"auth-key,omitempty"`
|
|
ControlURL string `proxy:"control-url,omitempty"`
|
|
StateDir string `proxy:"state-dir,omitempty"`
|
|
Ephemeral bool `proxy:"ephemeral,omitempty"`
|
|
UDP bool `proxy:"udp,omitempty"`
|
|
|
|
AcceptRoutes *bool `proxy:"accept-routes,omitempty"`
|
|
ExitNode string `proxy:"exit-node,omitempty"`
|
|
ExitNodeAllowLANAccess *bool `proxy:"exit-node-allow-lan-access,omitempty"`
|
|
}
|
|
|
|
func init() {
|
|
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
|
|
hi.IPNVersion = C.MihomoName + " " + C.Version
|
|
})
|
|
envknob.SetNoLogsNoSupport()
|
|
if runtime.GOOS == "android" { // Android SDK 30 no longer permits Go's net.Interfaces to work (Issue 2293)
|
|
netmon.RegisterInterfaceGetter(func() (nif []netmon.Interface, err error) {
|
|
log.Debugln("[Tailscale] InterfaceGetter: start, IsForceAnet: %v", anet.IsForceAnet())
|
|
ifaces, err := anet.Interfaces()
|
|
if err != nil {
|
|
log.Warnln("[Tailscale] anet.Interfaces failed: %v", err)
|
|
return nil, err
|
|
}
|
|
for _, iff := range ifaces {
|
|
addrs, err := anet.InterfaceAddrsByInterface(&iff)
|
|
if err != nil {
|
|
log.Warnln("[Tailscale] anet.InterfaceAddrsByInterface(%v) failed: %v", iff.Name, err)
|
|
continue
|
|
}
|
|
nif = append(nif, netmon.Interface{
|
|
Interface: &net.Interface{
|
|
Index: iff.Index,
|
|
MTU: iff.MTU,
|
|
Name: iff.Name,
|
|
HardwareAddr: iff.HardwareAddr,
|
|
Flags: iff.Flags,
|
|
},
|
|
AltAddrs: addrs,
|
|
})
|
|
}
|
|
|
|
log.Debugln("[Tailscale] InterfaceGetter: %v", lo.Map(nif, func(item netmon.Interface, index int) string {
|
|
var addrs any
|
|
addrs, err := item.Addrs()
|
|
if err != nil {
|
|
addrs = err
|
|
}
|
|
return fmt.Sprintf("{Name: %s, Addrs: %v, IsUp: %v, IsLoopback: %v}", item.Name, addrs, item.IsUp(), item.IsLoopback())
|
|
}))
|
|
return
|
|
})
|
|
}
|
|
}
|
|
|
|
func NewTailscale(option TailscaleOption) (*Tailscale, error) {
|
|
if _, err := buildTailscaleMaskedPrefs(option); err != nil {
|
|
return nil, err
|
|
}
|
|
if option.StateDir == "" {
|
|
option.StateDir = "tailscale"
|
|
}
|
|
option.StateDir = C.Path.Resolve(option.StateDir)
|
|
if !C.Path.IsSafePath(option.StateDir) {
|
|
return nil, C.Path.ErrNotSafePath(option.StateDir)
|
|
}
|
|
|
|
addr := option.ControlURL
|
|
if addr == "" {
|
|
addr = "tailscale"
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
outbound := &Tailscale{
|
|
Base: NewBase(BaseOption{
|
|
Name: option.Name,
|
|
Addr: addr,
|
|
Type: C.Tailscale,
|
|
ProviderName: option.ProviderName,
|
|
UDP: option.UDP,
|
|
Interface: option.Interface,
|
|
RoutingMark: option.RoutingMark,
|
|
Prefer: option.IPVersion,
|
|
}),
|
|
option: option,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
backendInitCh: make(chan struct{}),
|
|
}
|
|
outbound.dialer = option.NewDialer(outbound.DialOptions())
|
|
outbound.server = &tsnet.Server{
|
|
Dir: option.StateDir,
|
|
Hostname: option.Hostname,
|
|
AuthKey: option.AuthKey,
|
|
ControlURL: option.ControlURL,
|
|
Ephemeral: option.Ephemeral,
|
|
SystemDialer: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
log.Debugln("[Tailscale](%s) SystemDialer: start dial %s %s", option.Name, network, address)
|
|
conn, err := outbound.dialer.DialContext(ctx, network, address)
|
|
log.Debugln("[Tailscale](%s) SystemDialer: finish dial %s %s, err: %v", option.Name, network, address, err)
|
|
return conn, err
|
|
},
|
|
SystemPacketListener: func(ctx context.Context, network, address string) (net.PacketConn, error) {
|
|
log.Debugln("[Tailscale](%s) SystemPacketListener: start listen %s %s", option.Name, network, address)
|
|
pc, err := outbound.dialer.ListenPacket(ctx, network, address, netip.AddrPort{})
|
|
log.Debugln("[Tailscale](%s) SystemPacketListener: finish listen %s %s, err: %v", option.Name, network, address, err)
|
|
return pc, err
|
|
},
|
|
ExtraRootCAs: ca.GetCertPool(),
|
|
LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) {
|
|
log.Debugln("[Tailscale](%s) LookupHook: start lookup %s", option.Name, host)
|
|
ips, err := resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
|
|
log.Debugln("[Tailscale](%s) LookupHook: finish lookup %s, ips: %v, err: %v", option.Name, host, ips, err)
|
|
return ips, err
|
|
},
|
|
UserLogf: func(format string, args ...any) {
|
|
log.Infoln("[Tailscale](%s) %s", option.Name, fmt.Sprintf(format, args...))
|
|
},
|
|
Logf: func(format string, args ...any) {
|
|
log.Debugln("[Tailscale](%s) %s", option.Name, fmt.Sprintf(format, args...))
|
|
},
|
|
}
|
|
dnsTransport := tailscaleDNSTransport{tailscale: outbound}
|
|
outbound.dnsResolver = dns.NewResolverFromClient(dnsTransport)
|
|
outbound.unregisterDNSResolver = dns.RegisterTailscaleDnsClient(option.Name, dnsTransport)
|
|
return outbound, nil
|
|
}
|
|
|
|
func (t *Tailscale) start() error {
|
|
t.startOnce.Do(func() {
|
|
if err := t.server.Start(); err != nil {
|
|
t.startErr = err
|
|
t.setBackendInitialized(err)
|
|
return
|
|
}
|
|
t.serverStarted = true
|
|
ctx, cancel := context.WithTimeout(t.ctx, 30*time.Second)
|
|
defer cancel()
|
|
if err := t.applyPrefs(ctx); err != nil {
|
|
t.startErr = err
|
|
t.setBackendInitialized(err)
|
|
return
|
|
}
|
|
go t.watchBackendState()
|
|
})
|
|
return t.startErr
|
|
}
|
|
|
|
func (t *Tailscale) ensureStarted(ctx context.Context) error {
|
|
if err := t.start(); err != nil {
|
|
return err
|
|
}
|
|
return t.waitBackendInitialized(ctx)
|
|
}
|
|
|
|
func (t *Tailscale) watchBackendState() {
|
|
lc, err := t.server.LocalClient()
|
|
if err != nil {
|
|
t.setBackendInitialized(err)
|
|
return
|
|
}
|
|
watcher, err := lc.WatchIPNBus(t.ctx, ipn.NotifyInitialState)
|
|
if err != nil {
|
|
t.setBackendInitialized(err)
|
|
return
|
|
}
|
|
defer watcher.Close()
|
|
|
|
backendInitialized := false
|
|
exitNodeNeedsStatus := tailscaleExitNodeNeedsStatus(t.option)
|
|
for {
|
|
n, err := watcher.Next()
|
|
if err != nil {
|
|
t.setBackendInitialized(err)
|
|
return
|
|
}
|
|
if n.State == nil {
|
|
continue
|
|
}
|
|
|
|
if *n.State != ipn.NoState && !backendInitialized {
|
|
t.setBackendInitialized(nil)
|
|
backendInitialized = true
|
|
if !exitNodeNeedsStatus {
|
|
return
|
|
}
|
|
}
|
|
if exitNodeNeedsStatus && *n.State == ipn.Running {
|
|
if err := t.applyExitNodePrefs(t.ctx); err != nil {
|
|
log.Warnln("[Tailscale](%s) set exit node failed: %v", t.Name(), err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Tailscale) setBackendInitialized(err error) {
|
|
t.backendInitOnce.Do(func() {
|
|
t.backendInitErr = err
|
|
close(t.backendInitCh)
|
|
})
|
|
}
|
|
|
|
func (t *Tailscale) waitBackendInitialized(ctx context.Context) error {
|
|
select {
|
|
case <-t.backendInitCh:
|
|
return t.backendInitErr
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-t.ctx.Done():
|
|
return t.ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (t *Tailscale) applyPrefs(ctx context.Context) error {
|
|
mp, err := buildTailscaleMaskedPrefs(t.option)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mp == nil {
|
|
return nil
|
|
}
|
|
lc, err := t.server.LocalClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = lc.EditPrefs(ctx, mp)
|
|
return err
|
|
}
|
|
|
|
func (t *Tailscale) applyExitNodePrefs(ctx context.Context) error {
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
lc, err := t.server.LocalClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status, err := lc.Status(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mp := &ipn.MaskedPrefs{
|
|
ExitNodeIPSet: true,
|
|
}
|
|
if t.option.ExitNodeAllowLANAccess != nil {
|
|
mp.ExitNodeAllowLANAccess = *t.option.ExitNodeAllowLANAccess
|
|
mp.ExitNodeAllowLANAccessSet = true
|
|
}
|
|
if err = mp.SetExitNodeIP(t.option.ExitNode, status); err != nil {
|
|
return err
|
|
}
|
|
_, err = lc.EditPrefs(ctx, mp)
|
|
return err
|
|
}
|
|
|
|
func buildTailscaleMaskedPrefs(option TailscaleOption) (*ipn.MaskedPrefs, error) {
|
|
var mp ipn.MaskedPrefs
|
|
changed := false
|
|
|
|
if option.AcceptRoutes != nil {
|
|
mp.RouteAll = *option.AcceptRoutes
|
|
mp.RouteAllSet = true
|
|
changed = true
|
|
}
|
|
if option.ExitNode != "" {
|
|
if autoExitNode, ok := ipn.ParseAutoExitNodeString(option.ExitNode); ok {
|
|
mp.AutoExitNode = autoExitNode
|
|
mp.AutoExitNodeSet = true
|
|
changed = true
|
|
}
|
|
}
|
|
if option.ExitNodeAllowLANAccess != nil && !tailscaleExitNodeNeedsStatus(option) {
|
|
mp.ExitNodeAllowLANAccess = *option.ExitNodeAllowLANAccess
|
|
mp.ExitNodeAllowLANAccessSet = true
|
|
changed = true
|
|
}
|
|
if !changed {
|
|
return nil, nil
|
|
}
|
|
return &mp, nil
|
|
}
|
|
|
|
func tailscaleExitNodeNeedsStatus(option TailscaleOption) bool {
|
|
if option.ExitNode == "" {
|
|
return false
|
|
}
|
|
_, ok := ipn.ParseAutoExitNodeString(option.ExitNode)
|
|
return !ok
|
|
}
|
|
|
|
func (t *Tailscale) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
|
if err = t.ensureStarted(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
netStack, err := t.server.Netstack(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v4, v6 := t.server.TailscaleIPs()
|
|
options := t.DialOptions()
|
|
options = append(options, dialer.WithResolver(t.dnsResolver))
|
|
options = append(options, dialer.WithNetDialer(dialer.NetDialerFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
dst, err := netip.ParseAddrPort(address) // the dialer will resolve the domain to ip
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
src := v4
|
|
if dst.Addr().Is6() {
|
|
src = v6
|
|
}
|
|
tcpConn, err := netStack.DialContextTCPWithBind(ctx, src, dst)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tcpConn, nil
|
|
})))
|
|
var conn net.Conn
|
|
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if conn == nil {
|
|
return nil, errors.New("conn is nil")
|
|
}
|
|
return NewConn(conn, t), nil
|
|
}
|
|
|
|
func (t *Tailscale) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|
if err = t.ensureStarted(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
|
return nil, err
|
|
}
|
|
v4, v6 := t.server.TailscaleIPs()
|
|
src := v4
|
|
if metadata.DstIP.Is6() {
|
|
src = v6
|
|
}
|
|
pc, err := t.server.ListenPacket("udp", net.JoinHostPort(src.String(), "0"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pc == nil {
|
|
return nil, errors.New("packetConn is nil")
|
|
}
|
|
return NewPacketConn(pc, t), nil
|
|
}
|
|
|
|
func (t *Tailscale) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
|
if metadata.Host != "" {
|
|
ip, err := resolveIPWithResolver(ctx, metadata.Host, t.prefer, t.dnsResolver)
|
|
if err != nil {
|
|
return fmt.Errorf("can't resolve ip: %w", err)
|
|
}
|
|
metadata.DstIP = ip
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type tailscaleDNSTransport struct {
|
|
tailscale *Tailscale
|
|
}
|
|
|
|
func (t tailscaleDNSTransport) Address() string {
|
|
return "tailscale://" + t.tailscale.Name()
|
|
}
|
|
|
|
func (t tailscaleDNSTransport) ResetConnection() {}
|
|
|
|
func (t tailscaleDNSTransport) ExchangeContext(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
|
|
if len(msg.Question) == 0 {
|
|
return nil, errors.New("should have one question at least")
|
|
}
|
|
if err := t.tailscale.ensureStarted(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
q := msg.Question[0]
|
|
qtypeName, ok := D.TypeToString[q.Qtype]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported query type: %d", q.Qtype)
|
|
}
|
|
lc, err := t.tailscale.server.LocalClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response, _, err := lc.QueryDNS(ctx, q.Name, qtypeName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var responseMsg D.Msg
|
|
if err = responseMsg.Unpack(response); err != nil {
|
|
return nil, err
|
|
}
|
|
responseMsg.Id = msg.Id
|
|
return &responseMsg, nil
|
|
}
|
|
|
|
func (t *Tailscale) ProxyInfo() C.ProxyInfo {
|
|
info := t.Base.ProxyInfo()
|
|
info.DialerProxy = t.option.DialerProxy
|
|
return info
|
|
}
|
|
|
|
func (t *Tailscale) IsL3Protocol(metadata *C.Metadata) bool {
|
|
return true
|
|
}
|
|
|
|
func (t *Tailscale) Close() error {
|
|
t.cancel()
|
|
if t.unregisterDNSResolver != nil {
|
|
t.unregisterDNSResolver()
|
|
}
|
|
t.startOnce.Do(func() {
|
|
t.startErr = errors.New("tailscale outbound closed")
|
|
})
|
|
if t.server != nil && t.serverStarted { // tsnet.Server.Close() must not be called before or concurrently with Start.
|
|
return t.server.Close()
|
|
}
|
|
return nil
|
|
}
|