mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-29 12:01:29 +00:00
406 lines
13 KiB
Go
406 lines
13 KiB
Go
|
package tailscale
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"net/netip"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
"sync/atomic"
|
||
|
"syscall"
|
||
|
"time"
|
||
|
|
||
|
"github.com/sagernet/gvisor/pkg/tcpip"
|
||
|
"github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet"
|
||
|
"github.com/sagernet/gvisor/pkg/tcpip/header"
|
||
|
"github.com/sagernet/gvisor/pkg/tcpip/stack"
|
||
|
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
|
||
|
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
|
||
|
"github.com/sagernet/sing-box/adapter"
|
||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||
|
C "github.com/sagernet/sing-box/constant"
|
||
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||
|
"github.com/sagernet/sing-box/log"
|
||
|
"github.com/sagernet/sing-box/option"
|
||
|
"github.com/sagernet/sing-tun"
|
||
|
"github.com/sagernet/sing/common"
|
||
|
"github.com/sagernet/sing/common/bufio"
|
||
|
"github.com/sagernet/sing/common/control"
|
||
|
E "github.com/sagernet/sing/common/exceptions"
|
||
|
F "github.com/sagernet/sing/common/format"
|
||
|
"github.com/sagernet/sing/common/logger"
|
||
|
M "github.com/sagernet/sing/common/metadata"
|
||
|
N "github.com/sagernet/sing/common/network"
|
||
|
"github.com/sagernet/sing/service"
|
||
|
"github.com/sagernet/sing/service/filemanager"
|
||
|
"github.com/sagernet/tailscale/ipn"
|
||
|
"github.com/sagernet/tailscale/net/netmon"
|
||
|
"github.com/sagernet/tailscale/net/tsaddr"
|
||
|
"github.com/sagernet/tailscale/tsnet"
|
||
|
"github.com/sagernet/tailscale/types/ipproto"
|
||
|
"github.com/sagernet/tailscale/version"
|
||
|
"github.com/sagernet/tailscale/wgengine/filter"
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
version.SetVersion("sing-box " + C.Version)
|
||
|
}
|
||
|
|
||
|
func RegisterEndpoint(registry *endpoint.Registry) {
|
||
|
endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, NewEndpoint)
|
||
|
}
|
||
|
|
||
|
type Endpoint struct {
|
||
|
endpoint.Adapter
|
||
|
ctx context.Context
|
||
|
router adapter.Router
|
||
|
logger logger.ContextLogger
|
||
|
network adapter.NetworkManager
|
||
|
platformInterface platform.Interface
|
||
|
server *tsnet.Server
|
||
|
stack *stack.Stack
|
||
|
filter *atomic.Pointer[filter.Filter]
|
||
|
|
||
|
exitNode string
|
||
|
exitNodeAllowLANAccess bool
|
||
|
advertiseRoutes []netip.Prefix
|
||
|
advertiseExitNode bool
|
||
|
|
||
|
udpTimeout time.Duration
|
||
|
}
|
||
|
|
||
|
func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) {
|
||
|
stateDirectory := options.StateDirectory
|
||
|
if stateDirectory == "" {
|
||
|
stateDirectory = "tailscale"
|
||
|
}
|
||
|
hostname := options.Hostname
|
||
|
if hostname == "" {
|
||
|
osHostname, _ := os.Hostname()
|
||
|
osHostname = strings.TrimSpace(osHostname)
|
||
|
hostname = osHostname
|
||
|
}
|
||
|
if hostname == "" {
|
||
|
hostname = "sing-box"
|
||
|
}
|
||
|
stateDirectory = filemanager.BasePath(ctx, os.ExpandEnv(stateDirectory))
|
||
|
stateDirectory, _ = filepath.Abs(stateDirectory)
|
||
|
server := &tsnet.Server{
|
||
|
Dir: stateDirectory,
|
||
|
Hostname: hostname,
|
||
|
Logf: func(format string, args ...any) {
|
||
|
logger.Trace(fmt.Sprintf(format, args...))
|
||
|
},
|
||
|
UserLogf: func(format string, args ...any) {
|
||
|
logger.Debug(fmt.Sprintf(format, args...))
|
||
|
},
|
||
|
Ephemeral: options.Ephemeral,
|
||
|
AuthKey: options.AuthKey,
|
||
|
ControlURL: options.ControlURL,
|
||
|
}
|
||
|
for _, advertiseRoute := range options.AdvertiseRoutes {
|
||
|
if advertiseRoute.Addr().IsUnspecified() && advertiseRoute.Bits() == 0 {
|
||
|
return nil, E.New("`advertise_routes` cannot be default, use `advertise_exit_node` instead.")
|
||
|
}
|
||
|
}
|
||
|
if options.AdvertiseExitNode && options.ExitNode != "" {
|
||
|
return nil, E.New("cannot advertise an exit node and use an exit node at the same time.")
|
||
|
}
|
||
|
return &Endpoint{
|
||
|
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
||
|
ctx: ctx,
|
||
|
router: router,
|
||
|
logger: logger,
|
||
|
network: service.FromContext[adapter.NetworkManager](ctx),
|
||
|
platformInterface: service.FromContext[platform.Interface](ctx),
|
||
|
server: server,
|
||
|
exitNode: options.ExitNode,
|
||
|
exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess,
|
||
|
advertiseRoutes: options.AdvertiseRoutes,
|
||
|
advertiseExitNode: options.AdvertiseExitNode,
|
||
|
udpTimeout: time.Duration(options.UDPTimeout),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||
|
if stage != adapter.StartStateStart {
|
||
|
return nil
|
||
|
}
|
||
|
if t.platformInterface != nil {
|
||
|
err := t.network.UpdateInterfaces()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
netmon.RegisterInterfaceGetter(func() ([]netmon.Interface, error) {
|
||
|
return common.Map(t.network.InterfaceFinder().Interfaces(), func(it control.Interface) netmon.Interface {
|
||
|
return netmon.Interface{
|
||
|
Interface: &net.Interface{
|
||
|
Index: it.Index,
|
||
|
MTU: it.MTU,
|
||
|
Name: it.Name,
|
||
|
HardwareAddr: it.HardwareAddr,
|
||
|
Flags: it.Flags,
|
||
|
},
|
||
|
AltAddrs: common.Map(it.Addresses, func(it netip.Prefix) net.Addr {
|
||
|
return &net.IPNet{
|
||
|
IP: it.Addr().AsSlice(),
|
||
|
Mask: net.CIDRMask(it.Bits(), it.Addr().BitLen()),
|
||
|
}
|
||
|
}),
|
||
|
}
|
||
|
}), nil
|
||
|
})
|
||
|
if runtime.GOOS == "android" {
|
||
|
setAndroidProtectFunc(t.platformInterface)
|
||
|
}
|
||
|
}
|
||
|
err := t.server.Start()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
ipStack := t.server.ExportNetstack().ExportIPStack()
|
||
|
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
|
||
|
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout).HandlePacket)
|
||
|
t.stack = ipStack
|
||
|
|
||
|
localBackend := t.server.ExportLocalBackend()
|
||
|
perfs := &ipn.MaskedPrefs{
|
||
|
ExitNodeIPSet: true,
|
||
|
AdvertiseRoutesSet: true,
|
||
|
}
|
||
|
if len(t.advertiseRoutes) > 0 {
|
||
|
perfs.AdvertiseRoutes = t.advertiseRoutes
|
||
|
}
|
||
|
if t.advertiseExitNode {
|
||
|
perfs.AdvertiseRoutes = append(perfs.AdvertiseRoutes, tsaddr.ExitRoutes()...)
|
||
|
}
|
||
|
_, err = localBackend.EditPrefs(perfs)
|
||
|
if err != nil {
|
||
|
return E.Cause(err, "update prefs")
|
||
|
}
|
||
|
t.filter = localBackend.ExportFilter()
|
||
|
|
||
|
go t.watchState()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) watchState() {
|
||
|
localBackend := t.server.ExportLocalBackend()
|
||
|
localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||
|
if roNotify.State != nil && *roNotify.State != ipn.NeedsLogin && *roNotify.State != ipn.NoState {
|
||
|
return false
|
||
|
}
|
||
|
authURL := localBackend.StatusWithoutPeers().AuthURL
|
||
|
if authURL != "" {
|
||
|
t.logger.Info("Waiting for authentication: ", authURL)
|
||
|
if t.platformInterface != nil {
|
||
|
err := t.platformInterface.SendNotification(&platform.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)
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
})
|
||
|
if t.exitNode != "" {
|
||
|
localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||
|
if roNotify.State == nil || *roNotify.State != ipn.Running {
|
||
|
return true
|
||
|
}
|
||
|
status, err := common.Must1(t.server.LocalClient()).Status(t.ctx)
|
||
|
if err != nil {
|
||
|
t.logger.Error("set exit node: ", err)
|
||
|
return
|
||
|
}
|
||
|
perfs := &ipn.MaskedPrefs{
|
||
|
Prefs: ipn.Prefs{
|
||
|
ExitNodeAllowLANAccess: t.exitNodeAllowLANAccess,
|
||
|
},
|
||
|
ExitNodeIPSet: true,
|
||
|
ExitNodeAllowLANAccessSet: true,
|
||
|
}
|
||
|
err = perfs.SetExitNodeIP(t.exitNode, status)
|
||
|
if err != nil {
|
||
|
t.logger.Error("set exit node: ", err)
|
||
|
return true
|
||
|
}
|
||
|
_, err = localBackend.EditPrefs(perfs)
|
||
|
if err != nil {
|
||
|
t.logger.Error("set exit node: ", err)
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) Close() error {
|
||
|
netmon.RegisterInterfaceGetter(nil)
|
||
|
if runtime.GOOS == "android" {
|
||
|
setAndroidProtectFunc(nil)
|
||
|
}
|
||
|
return common.Close(common.PtrOrNil(t.server))
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||
|
switch network {
|
||
|
case N.NetworkTCP:
|
||
|
t.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||
|
case N.NetworkUDP:
|
||
|
t.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||
|
}
|
||
|
if destination.IsFqdn() {
|
||
|
destinationAddresses, err := t.router.LookupDefault(ctx, destination.Fqdn)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return N.DialSerial(ctx, t, network, destination, destinationAddresses)
|
||
|
}
|
||
|
addr := tcpip.FullAddress{
|
||
|
NIC: 1,
|
||
|
Port: destination.Port,
|
||
|
Addr: addressFromAddr(destination.Addr),
|
||
|
}
|
||
|
var networkProtocol tcpip.NetworkProtocolNumber
|
||
|
if destination.IsIPv4() {
|
||
|
networkProtocol = header.IPv4ProtocolNumber
|
||
|
} else {
|
||
|
networkProtocol = header.IPv6ProtocolNumber
|
||
|
}
|
||
|
switch N.NetworkName(network) {
|
||
|
case N.NetworkTCP:
|
||
|
tcpConn, err := gonet.DialContextTCP(ctx, t.stack, addr, networkProtocol)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return tcpConn, nil
|
||
|
case N.NetworkUDP:
|
||
|
udpConn, err := gonet.DialUDP(t.stack, nil, &addr, networkProtocol)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return udpConn, nil
|
||
|
default:
|
||
|
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||
|
t.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||
|
if destination.IsFqdn() {
|
||
|
destinationAddresses, err := t.router.LookupDefault(ctx, destination.Fqdn)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
packetConn, _, err := N.ListenSerial(ctx, t, destination, destinationAddresses)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return packetConn, err
|
||
|
}
|
||
|
addr4, addr6 := t.server.TailscaleIPs()
|
||
|
bind := tcpip.FullAddress{
|
||
|
NIC: 1,
|
||
|
}
|
||
|
var networkProtocol tcpip.NetworkProtocolNumber
|
||
|
if !addr6.IsValid() && addr4.IsValid() {
|
||
|
networkProtocol = header.IPv4ProtocolNumber
|
||
|
bind.Addr = addressFromAddr(addr4)
|
||
|
} else {
|
||
|
networkProtocol = header.IPv6ProtocolNumber
|
||
|
bind.Addr = addressFromAddr(addr6)
|
||
|
}
|
||
|
udpConn, err := gonet.DialUDP(t.stack, &bind, nil, networkProtocol)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return udpConn, nil
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
|
||
|
tsFilter := t.filter.Load()
|
||
|
if tsFilter != nil {
|
||
|
var ipProto ipproto.Proto
|
||
|
switch N.NetworkName(network) {
|
||
|
case N.NetworkTCP:
|
||
|
ipProto = ipproto.TCP
|
||
|
case N.NetworkUDP:
|
||
|
ipProto = ipproto.UDP
|
||
|
}
|
||
|
response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto)
|
||
|
switch response {
|
||
|
case filter.Drop:
|
||
|
return syscall.ECONNRESET
|
||
|
case filter.DropSilently:
|
||
|
return tun.ErrDrop
|
||
|
}
|
||
|
}
|
||
|
return t.router.PreMatch(adapter.InboundContext{
|
||
|
Inbound: t.Tag(),
|
||
|
InboundType: t.Type(),
|
||
|
Network: network,
|
||
|
Source: source,
|
||
|
Destination: destination,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||
|
var metadata adapter.InboundContext
|
||
|
metadata.Inbound = t.Tag()
|
||
|
metadata.InboundType = t.Type()
|
||
|
metadata.Source = source
|
||
|
addr4, addr6 := t.server.TailscaleIPs()
|
||
|
switch destination.Addr {
|
||
|
case addr4:
|
||
|
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
|
||
|
case addr6:
|
||
|
destination.Addr = netip.IPv6Loopback()
|
||
|
}
|
||
|
metadata.Destination = destination
|
||
|
t.logger.InfoContext(ctx, "inbound connection from ", source)
|
||
|
t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
|
||
|
t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||
|
}
|
||
|
|
||
|
func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||
|
var metadata adapter.InboundContext
|
||
|
metadata.Inbound = t.Tag()
|
||
|
metadata.InboundType = t.Type()
|
||
|
metadata.Source = source
|
||
|
metadata.Destination = destination
|
||
|
addr4, addr6 := t.server.TailscaleIPs()
|
||
|
switch destination.Addr {
|
||
|
case addr4:
|
||
|
metadata.OriginDestination = destination
|
||
|
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
|
||
|
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
||
|
case addr6:
|
||
|
metadata.OriginDestination = destination
|
||
|
destination.Addr = netip.IPv6Loopback()
|
||
|
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
||
|
}
|
||
|
t.logger.InfoContext(ctx, "inbound packet connection from ", source)
|
||
|
t.logger.InfoContext(ctx, "inbound packet connection to ", destination)
|
||
|
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||
|
}
|
||
|
|
||
|
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
||
|
if destination.Is6() {
|
||
|
return tcpip.AddrFrom16(destination.As16())
|
||
|
} else {
|
||
|
return tcpip.AddrFrom4(destination.As4())
|
||
|
}
|
||
|
}
|