mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-10-22 12:02:47 +00:00
384 lines
12 KiB
Go
384 lines
12 KiB
Go
package masque
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
|
"github.com/sagernet/sing-box/common/dialer"
|
|
"github.com/sagernet/sing-box/common/iponly"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
"github.com/sagernet/sing-box/service/oomkiller"
|
|
"github.com/sagernet/sing-box/transport/device"
|
|
"github.com/sagernet/sing-box/transport/http"
|
|
"github.com/sagernet/sing-box/transport/masque"
|
|
"github.com/sagernet/sing-tun"
|
|
"github.com/sagernet/sing-tun/gtcpip/header"
|
|
"github.com/sagernet/sing/common"
|
|
"github.com/sagernet/sing/common/buf"
|
|
"github.com/sagernet/sing/common/bufio"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/json/badoption"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
"github.com/sagernet/sing/service"
|
|
)
|
|
|
|
const defaultKeepAlivePeriod = 10 * time.Second
|
|
|
|
var (
|
|
_ adapter.OutboundWithPreferredRoutes = (*ClientEndpoint)(nil)
|
|
_ adapter.FlowOutbound = (*ClientEndpoint)(nil)
|
|
_ adapter.InterfaceUpdateListener = (*ClientEndpoint)(nil)
|
|
_ adapter.OnDemandEndpoint = (*ClientEndpoint)(nil)
|
|
_ dialer.PacketDialerWithDestination = (*ClientEndpoint)(nil)
|
|
_ masque.ClientHandler = (*ClientEndpoint)(nil)
|
|
)
|
|
|
|
type ClientEndpoint struct {
|
|
endpointBase
|
|
ctx context.Context
|
|
dnsRouter adapter.DNSRouter
|
|
client *masque.Client
|
|
deviceOptions *device.Options
|
|
device device.Device
|
|
mtu uint32
|
|
onDemand bool
|
|
stateAccess sync.Mutex
|
|
deviceStarted bool
|
|
state atomic.Pointer[clientState]
|
|
}
|
|
|
|
type clientState struct {
|
|
configured bool
|
|
localAddresses []netip.Prefix
|
|
routes []masque.AddressRange
|
|
}
|
|
|
|
func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MASQUEClientEndpointOptions) (adapter.Endpoint, error) {
|
|
if options.MTU == 0 {
|
|
options.MTU = masque.DefaultMTU
|
|
}
|
|
version := options.ResolvedVersion()
|
|
http2Options := options.HTTP2Options
|
|
if version == 3 {
|
|
http2Options = options.HTTP3Options.HTTP2Options
|
|
if options.Version == 0 && http.NewHTTP3Client == nil {
|
|
version = 2
|
|
}
|
|
}
|
|
if http2Options.KeepAlivePeriod == 0 {
|
|
http2Options.KeepAlivePeriod = badoption.Duration(defaultKeepAlivePeriod)
|
|
}
|
|
options.HTTP3Options.HTTP2Options = http2Options
|
|
if options.HTTP3Options.InitialPacketSize == 0 {
|
|
options.HTTP3Options.InitialPacketSize = min(int(options.MTU)+masque.QUICPacketOverhead, math.MaxUint16)
|
|
}
|
|
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
|
|
Context: ctx,
|
|
Options: options.DialerOptions,
|
|
RemoteIsDomain: options.ServerIsDomain(),
|
|
ResolverOnDetour: true,
|
|
NewDialer: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headers := options.Headers.Build()
|
|
authority := headers.Get("Host")
|
|
headers.Del("Host")
|
|
if authority == "" {
|
|
server := options.ServerOptions.Build()
|
|
authority = server.String()
|
|
if server.Port == 443 {
|
|
authority = server.AddrString()
|
|
if server.IsIPv6() {
|
|
authority = "[" + authority + "]"
|
|
}
|
|
}
|
|
}
|
|
httpClient, err := http.NewClientWithTLS(ctx, logger, outboundDialer, options.ServerOptions, common.PtrValueOrDefault(options.TLS), http.ClientOptions{
|
|
Authority: authority,
|
|
Username: options.Username,
|
|
Password: options.Password,
|
|
Headers: headers,
|
|
Version: version,
|
|
DisableVersionFallback: options.DisableVersionFallback,
|
|
HTTP2Options: http2Options,
|
|
HTTP3Options: options.HTTP3Options,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientEndpoint := &ClientEndpoint{
|
|
endpointBase: endpointBase{
|
|
Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeMASQUEClient, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions),
|
|
router: router,
|
|
logger: logger,
|
|
},
|
|
ctx: ctx,
|
|
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
|
mtu: options.MTU,
|
|
onDemand: options.OnDemand,
|
|
}
|
|
clientEndpoint.state.Store(&clientState{})
|
|
clientEndpoint.deviceOptions = newDeviceOptions(ctx, logger, clientEndpoint, options.MASQUEEndpointOptions, time.Duration(options.UDPTimeout), nil)
|
|
clientEndpoint.client, err = masque.NewClient(masque.ClientOptions{
|
|
Context: ctx,
|
|
Logger: logger,
|
|
HTTPClient: httpClient,
|
|
Path: options.Path,
|
|
AdvertiseRoutes: options.AdvertiseRoutes,
|
|
Handler: clientEndpoint,
|
|
})
|
|
if err != nil {
|
|
httpClient.Close()
|
|
return nil, err
|
|
}
|
|
return clientEndpoint, nil
|
|
}
|
|
|
|
func (c *ClientEndpoint) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
|
switch stage {
|
|
case adapter.StartStateInitialize:
|
|
c.deviceOptions.MemoryPressure = oomkiller.MemoryPressure(c.ctx)
|
|
tunnelDevice, err := device.New(*c.deviceOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scope.Add(tunnelDevice.Close)
|
|
tunnelDevice.SetPacketWriter(c.writePacketBuffers)
|
|
c.device = tunnelDevice
|
|
c.deviceOptions = nil
|
|
scope.Add(c.client.Close)
|
|
case adapter.StartStatePostStart:
|
|
c.client.Start()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ClientEndpoint) UpdateConfiguration(configuration masque.Configuration) error {
|
|
c.stateAccess.Lock()
|
|
defer c.stateAccess.Unlock()
|
|
err := c.device.UpdateConfiguration(device.Configuration{
|
|
MTU: c.mtu,
|
|
Address: configuration.Address,
|
|
})
|
|
if err != nil {
|
|
return E.Cause(err, "update device configuration")
|
|
}
|
|
if !c.deviceStarted {
|
|
err = c.device.Start()
|
|
if err != nil {
|
|
return E.Cause(err, "start device")
|
|
}
|
|
c.deviceStarted = true
|
|
}
|
|
if !slices.Equal(c.state.Load().localAddresses, configuration.Address) {
|
|
c.logger.Info("assigned ", strings.Join(common.Map(configuration.Address, netip.Prefix.String), " "))
|
|
}
|
|
c.state.Store(&clientState{
|
|
configured: true,
|
|
localAddresses: configuration.Address,
|
|
routes: configuration.Routes,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (c *ClientEndpoint) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
|
|
if !c.state.Load().configured {
|
|
buf.ReleaseMulti(packetBuffers)
|
|
return nil
|
|
}
|
|
err := c.device.WriteInboundBuffers(packetBuffers)
|
|
buf.ReleaseMulti(packetBuffers)
|
|
return err
|
|
}
|
|
|
|
func (c *ClientEndpoint) FrontHeadroom() int {
|
|
return c.device.FrontHeadroom()
|
|
}
|
|
|
|
func (c *ClientEndpoint) InterfaceUpdated(ctx context.Context) {
|
|
c.client.RestartSession()
|
|
}
|
|
|
|
func (c *ClientEndpoint) OnDemand() bool {
|
|
return c.onDemand
|
|
}
|
|
|
|
func (c *ClientEndpoint) SetKeepIdleConnections(keep bool) {
|
|
if keep {
|
|
c.client.Resume()
|
|
} else {
|
|
c.client.Suspend()
|
|
}
|
|
}
|
|
|
|
func (c *ClientEndpoint) waitReady(ctx context.Context) error {
|
|
if !c.onDemand {
|
|
if !c.client.Ready() {
|
|
return E.New("endpoint is not ready yet")
|
|
}
|
|
return nil
|
|
}
|
|
c.client.Resume()
|
|
waitCtx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
|
defer cancel()
|
|
return c.client.WaitReady(waitCtx)
|
|
}
|
|
|
|
func (c *ClientEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
|
|
return adapter.PreMatchFlow
|
|
}
|
|
|
|
func (c *ClientEndpoint) PortAddresses() (netip.Addr, netip.Addr) {
|
|
return c.device.PortAddresses()
|
|
}
|
|
|
|
func (c *ClientEndpoint) PortMTU() uint32 {
|
|
return c.device.PortMTU()
|
|
}
|
|
|
|
func (c *ClientEndpoint) UpstreamPort() any {
|
|
return c.device
|
|
}
|
|
|
|
func (c *ClientEndpoint) AttachReturn(returnPath tun.Return) error {
|
|
return c.device.AttachReturn(returnPath)
|
|
}
|
|
|
|
func (c *ClientEndpoint) DetachReturn(returnPath tun.Return) error {
|
|
return c.device.DetachReturn(returnPath)
|
|
}
|
|
|
|
func (c *ClientEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
|
|
return c.judgeFlow(c, c.state.Load().localAddresses, network, source, destination, firstPacket)
|
|
}
|
|
|
|
func (c *ClientEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
|
|
c.newDNSPacket(log.ContextWithNewID(c.ctx), c, payload, source, destination, writer)
|
|
}
|
|
|
|
func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
|
|
if c.onDemand {
|
|
c.client.Resume()
|
|
}
|
|
if !c.client.Ready() {
|
|
return E.New("endpoint is not ready yet")
|
|
}
|
|
return c.client.WritePacketBuffers(common.Map(packets, func(packet []byte) *buf.Buffer {
|
|
packetBuffer := buf.NewSize(masque.PacketHeadroom + len(packet))
|
|
packetBuffer.Resize(masque.PacketHeadroom, 0)
|
|
common.Must1(packetBuffer.Write(packet))
|
|
return packetBuffer
|
|
}), true)
|
|
}
|
|
|
|
func (c *ClientEndpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error {
|
|
if c.onDemand {
|
|
c.client.Resume()
|
|
}
|
|
return c.client.WritePacketBuffers(packetBuffers, false)
|
|
}
|
|
|
|
func (c *ClientEndpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
|
c.newConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose)
|
|
}
|
|
|
|
func (c *ClientEndpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
|
c.newPacketConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose)
|
|
}
|
|
|
|
func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
switch network {
|
|
case N.NetworkTCP:
|
|
c.logger.InfoContext(ctx, "outbound connection to ", destination)
|
|
case N.NetworkUDP:
|
|
c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
|
}
|
|
err := c.waitReady(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if destination.IsDomain() {
|
|
destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
|
if lookupErr != nil {
|
|
return nil, lookupErr
|
|
}
|
|
return N.DialSerial(ctx, c.device, network, destination, destinationAddresses)
|
|
}
|
|
if !destination.Addr.IsValid() {
|
|
return nil, E.New("invalid destination: ", destination)
|
|
}
|
|
return c.device.DialContext(ctx, network, destination)
|
|
}
|
|
|
|
func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
|
|
c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
|
err := c.waitReady(ctx)
|
|
if err != nil {
|
|
return nil, netip.Addr{}, err
|
|
}
|
|
if destination.IsDomain() {
|
|
destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
|
if lookupErr != nil {
|
|
return nil, netip.Addr{}, lookupErr
|
|
}
|
|
packetConn, destinationAddress, listenErr := N.ListenSerial(ctx, c.device, destination, destinationAddresses)
|
|
if listenErr != nil {
|
|
return nil, netip.Addr{}, listenErr
|
|
}
|
|
return iponly.NewPacketConn(c.logger, packetConn), destinationAddress, nil
|
|
}
|
|
packetConn, err := c.device.ListenPacket(ctx, destination)
|
|
if err != nil {
|
|
return nil, netip.Addr{}, err
|
|
}
|
|
if destination.IsIP() {
|
|
return iponly.NewPacketConn(c.logger, packetConn), destination.Addr, nil
|
|
}
|
|
return iponly.NewPacketConn(c.logger, packetConn), netip.Addr{}, nil
|
|
}
|
|
|
|
func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
packetConn, destinationAddress, err := c.ListenPacketWithDestination(ctx, destination)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) {
|
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
|
}
|
|
return packetConn, nil
|
|
}
|
|
|
|
func (c *ClientEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
|
|
return false
|
|
}
|
|
|
|
func (c *ClientEndpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
|
|
state := c.state.Load()
|
|
if !state.configured || !c.client.Ready() {
|
|
return false
|
|
}
|
|
var protocol uint8
|
|
switch metadata.Network {
|
|
case N.NetworkTCP:
|
|
protocol = uint8(header.TCPProtocolNumber)
|
|
case N.NetworkUDP:
|
|
protocol = uint8(header.UDPProtocolNumber)
|
|
case N.NetworkICMP:
|
|
protocol = uint8(header.ICMPv4ProtocolNumber)
|
|
}
|
|
return masque.RoutesContain(state.routes, address, protocol)
|
|
}
|