mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-10-21 11:33:20 +00:00
479 lines
12 KiB
Go
479 lines
12 KiB
Go
package masque
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
C "github.com/sagernet/sing-box/constant"
|
|
transportHTTP "github.com/sagernet/sing-box/transport/http"
|
|
"github.com/sagernet/sing-tun"
|
|
"github.com/sagernet/sing/common/buf"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/logger"
|
|
)
|
|
|
|
const (
|
|
reconnectBackoffInitial = time.Second
|
|
reconnectBackoffMax = time.Minute
|
|
)
|
|
|
|
type Configuration struct {
|
|
Address []netip.Prefix
|
|
Routes []AddressRange
|
|
RoutesAdvertised bool
|
|
}
|
|
|
|
type ClientHandler interface {
|
|
UpdateConfiguration(configuration Configuration) error
|
|
WriteInboundBuffers(packetBuffers []*buf.Buffer) error
|
|
FrontHeadroom() int
|
|
}
|
|
|
|
type ClientOptions struct {
|
|
Context context.Context
|
|
Logger logger.ContextLogger
|
|
HTTPClient *transportHTTP.Client
|
|
Path string
|
|
AdvertiseRoutes []netip.Prefix
|
|
Handler ClientHandler
|
|
}
|
|
|
|
type Client struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
logger logger.ContextLogger
|
|
httpClient *transportHTTP.Client
|
|
template *Template
|
|
advertiseRoutes []AddressRange
|
|
handler ClientHandler
|
|
access sync.Mutex
|
|
current *clientSession
|
|
suspended bool
|
|
restarting bool
|
|
lastError error
|
|
stateUpdated chan struct{}
|
|
loopDone chan struct{}
|
|
}
|
|
|
|
type clientSession struct {
|
|
*session
|
|
client *Client
|
|
access sync.Mutex
|
|
configuration Configuration
|
|
ready bool
|
|
}
|
|
|
|
func NewClient(options ClientOptions) (*Client, error) {
|
|
template, err := ParseTemplate(options.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
advertiseRoutes, err := RangesFromPrefixes(options.AdvertiseRoutes, 0)
|
|
if err != nil {
|
|
return nil, E.Cause(err, "build advertised routes")
|
|
}
|
|
ctx, cancel := context.WithCancel(options.Context)
|
|
return &Client{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
logger: options.Logger,
|
|
httpClient: options.HTTPClient,
|
|
template: template,
|
|
advertiseRoutes: advertiseRoutes,
|
|
handler: options.Handler,
|
|
stateUpdated: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) Start() {
|
|
c.loopDone = make(chan struct{})
|
|
go c.loop()
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
c.cancel()
|
|
if c.loopDone != nil {
|
|
<-c.loopDone
|
|
}
|
|
return c.httpClient.Close()
|
|
}
|
|
|
|
func (c *Client) notifyStateLocked() {
|
|
close(c.stateUpdated)
|
|
c.stateUpdated = make(chan struct{})
|
|
}
|
|
|
|
func (c *Client) loop() {
|
|
defer close(c.loopDone)
|
|
backoff := reconnectBackoffInitial
|
|
for {
|
|
c.access.Lock()
|
|
suspended := c.suspended
|
|
stateUpdated := c.stateUpdated
|
|
c.restarting = false
|
|
if !suspended {
|
|
c.lastError = nil
|
|
}
|
|
c.access.Unlock()
|
|
if suspended {
|
|
select {
|
|
case <-stateUpdated:
|
|
continue
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
established, err := c.connect()
|
|
if c.ctx.Err() != nil {
|
|
return
|
|
}
|
|
c.access.Lock()
|
|
interrupted := c.suspended || c.restarting
|
|
if !interrupted {
|
|
c.lastError = err
|
|
}
|
|
c.notifyStateLocked()
|
|
stateUpdated = c.stateUpdated
|
|
c.access.Unlock()
|
|
if interrupted {
|
|
backoff = reconnectBackoffInitial
|
|
continue
|
|
}
|
|
if err != nil {
|
|
c.logger.Error(E.Cause(err, "connection closed"))
|
|
}
|
|
if established {
|
|
backoff = reconnectBackoffInitial
|
|
}
|
|
timer := time.NewTimer(backoff)
|
|
select {
|
|
case <-timer.C:
|
|
case <-stateUpdated:
|
|
timer.Stop()
|
|
case <-c.ctx.Done():
|
|
timer.Stop()
|
|
return
|
|
}
|
|
backoff = min(backoff*2, reconnectBackoffMax)
|
|
}
|
|
}
|
|
|
|
func (c *Client) connect() (bool, error) {
|
|
dialCtx, cancelDial := context.WithTimeout(c.ctx, C.TCPTimeout)
|
|
stream, err := c.httpClient.OpenTunnel(dialCtx, upgradeToken, c.template.Expand())
|
|
cancelDial()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
current := &clientSession{client: c}
|
|
c.access.Lock()
|
|
if c.suspended || c.restarting {
|
|
suspended := c.suspended
|
|
c.access.Unlock()
|
|
stream.Close()
|
|
if suspended {
|
|
c.httpClient.ResetConnections()
|
|
}
|
|
return false, nil
|
|
}
|
|
current.session = newSession(c.ctx, stream, current, c.handler.FrontHeadroom)
|
|
c.current = current
|
|
c.access.Unlock()
|
|
err = current.writeCapsule(newAddressCapsule(capsuleTypeAddressRequest, []AssignedAddress{
|
|
{RequestID: 1, Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 32)},
|
|
{RequestID: 2, Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 128)},
|
|
}))
|
|
if err == nil && len(c.advertiseRoutes) > 0 {
|
|
err = current.writeCapsule(newRouteCapsule(c.advertiseRoutes))
|
|
}
|
|
if err != nil {
|
|
current.cancel(err)
|
|
}
|
|
err = current.run()
|
|
c.access.Lock()
|
|
c.current = nil
|
|
c.access.Unlock()
|
|
current.access.Lock()
|
|
established := current.ready
|
|
current.ready = false
|
|
current.access.Unlock()
|
|
return established, err
|
|
}
|
|
|
|
func (c *Client) activeSession() *clientSession {
|
|
c.access.Lock()
|
|
defer c.access.Unlock()
|
|
return c.current
|
|
}
|
|
|
|
func (c *Client) Ready() bool {
|
|
current := c.activeSession()
|
|
if current == nil {
|
|
return false
|
|
}
|
|
current.access.Lock()
|
|
defer current.access.Unlock()
|
|
return current.ready
|
|
}
|
|
|
|
func (c *Client) WaitReady(ctx context.Context) error {
|
|
for {
|
|
c.access.Lock()
|
|
current := c.current
|
|
lastError := c.lastError
|
|
stateUpdated := c.stateUpdated
|
|
c.access.Unlock()
|
|
if current != nil {
|
|
current.access.Lock()
|
|
ready := current.ready
|
|
current.access.Unlock()
|
|
if ready {
|
|
return nil
|
|
}
|
|
} else if lastError != nil {
|
|
return lastError
|
|
}
|
|
select {
|
|
case <-stateUpdated:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-c.ctx.Done():
|
|
return c.ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) Suspend() {
|
|
c.access.Lock()
|
|
if c.suspended {
|
|
c.access.Unlock()
|
|
return
|
|
}
|
|
c.suspended = true
|
|
if c.current != nil {
|
|
c.current.cancel(E.New("suspended"))
|
|
}
|
|
c.notifyStateLocked()
|
|
c.access.Unlock()
|
|
c.httpClient.ResetConnections()
|
|
}
|
|
|
|
func (c *Client) Resume() {
|
|
c.access.Lock()
|
|
defer c.access.Unlock()
|
|
if !c.suspended {
|
|
return
|
|
}
|
|
c.suspended = false
|
|
c.lastError = nil
|
|
c.notifyStateLocked()
|
|
}
|
|
|
|
func (c *Client) RestartSession() {
|
|
c.access.Lock()
|
|
c.restarting = true
|
|
if c.current != nil {
|
|
c.current.cancel(E.New("network changed"))
|
|
}
|
|
c.lastError = nil
|
|
c.notifyStateLocked()
|
|
c.access.Unlock()
|
|
c.httpClient.ResetConnections()
|
|
}
|
|
|
|
func (c *Client) WritePacketBuffers(packetBuffers []*buf.Buffer, forwarded bool) error {
|
|
current := c.activeSession()
|
|
if current == nil {
|
|
buf.ReleaseMulti(packetBuffers)
|
|
return nil
|
|
}
|
|
current.access.Lock()
|
|
configuration := current.configuration
|
|
ready := current.ready
|
|
current.access.Unlock()
|
|
if !ready {
|
|
buf.ReleaseMulti(packetBuffers)
|
|
return nil
|
|
}
|
|
inet4Address, inet6Address := firstAddresses(configuration.Address)
|
|
var replies []*buf.Buffer
|
|
routedBuffers := packetBuffers[:0]
|
|
for _, packetBuffer := range packetBuffers {
|
|
_, destination, protocol, valid := packetAddresses(packetBuffer.Bytes())
|
|
if !valid {
|
|
packetBuffer.Release()
|
|
continue
|
|
}
|
|
errorType := tun.ICMPErrorNoRoute
|
|
routed := !configuration.RoutesAdvertised || RoutesContain(configuration.Routes, destination, protocol)
|
|
if routed && forwarded && !decrementHopLimit(packetBuffer.Bytes()) {
|
|
routed = false
|
|
errorType = tun.ICMPErrorHopLimitExceeded
|
|
}
|
|
if !routed {
|
|
reply, built := buildICMPError(packetBuffer.Bytes(), errorType, inet4Address, inet6Address, 0, c.handler.FrontHeadroom())
|
|
if built {
|
|
replies = append(replies, reply)
|
|
}
|
|
packetBuffer.Release()
|
|
continue
|
|
}
|
|
routedBuffers = append(routedBuffers, packetBuffer)
|
|
}
|
|
err := current.writePackets(routedBuffers)
|
|
if err != nil {
|
|
current.cancel(err)
|
|
}
|
|
if len(replies) > 0 {
|
|
return c.handler.WriteInboundBuffers(replies)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *clientSession) handleAddressAssign(addresses []AssignedAddress) error {
|
|
var assigned []netip.Prefix
|
|
for _, address := range addresses {
|
|
if address.RequestID != 0 && address.Prefix.Addr().IsUnspecified() && address.Prefix.IsSingleIP() {
|
|
continue
|
|
}
|
|
localAddress := address.Prefix.Addr()
|
|
if !address.Prefix.IsSingleIP() {
|
|
localAddress = localAddress.Next()
|
|
}
|
|
assigned = append(assigned, netip.PrefixFrom(localAddress, address.Prefix.Bits()))
|
|
}
|
|
s.access.Lock()
|
|
if slices.Equal(s.configuration.Address, assigned) {
|
|
s.access.Unlock()
|
|
return nil
|
|
}
|
|
s.configuration.Address = assigned
|
|
configuration := s.configuration
|
|
s.access.Unlock()
|
|
return s.updateConfiguration(configuration)
|
|
}
|
|
|
|
func (s *clientSession) handleRouteAdvertisement(routes []AddressRange) error {
|
|
s.access.Lock()
|
|
if s.configuration.RoutesAdvertised && slices.Equal(s.configuration.Routes, routes) {
|
|
s.access.Unlock()
|
|
return nil
|
|
}
|
|
s.configuration.Routes = routes
|
|
s.configuration.RoutesAdvertised = true
|
|
configuration := s.configuration
|
|
s.access.Unlock()
|
|
if len(configuration.Address) == 0 {
|
|
return nil
|
|
}
|
|
return s.updateConfiguration(configuration)
|
|
}
|
|
|
|
func (s *clientSession) updateConfiguration(configuration Configuration) error {
|
|
if len(configuration.Address) == 0 {
|
|
s.access.Lock()
|
|
s.ready = false
|
|
s.access.Unlock()
|
|
s.client.access.Lock()
|
|
s.client.notifyStateLocked()
|
|
s.client.access.Unlock()
|
|
return nil
|
|
}
|
|
err := s.client.handler.UpdateConfiguration(configuration)
|
|
if err != nil {
|
|
return E.Cause(err, "update configuration")
|
|
}
|
|
s.access.Lock()
|
|
s.ready = true
|
|
s.access.Unlock()
|
|
s.client.access.Lock()
|
|
s.client.lastError = nil
|
|
s.client.notifyStateLocked()
|
|
s.client.access.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *clientSession) handleAddressRequest(addresses []AssignedAddress) error {
|
|
rejections := make([]AssignedAddress, 0, len(addresses))
|
|
for _, address := range addresses {
|
|
unspecified := netip.IPv4Unspecified()
|
|
if address.Prefix.Addr().Is6() {
|
|
unspecified = netip.IPv6Unspecified()
|
|
}
|
|
rejections = append(rejections, AssignedAddress{RequestID: address.RequestID, Prefix: netip.PrefixFrom(unspecified, unspecified.BitLen())})
|
|
}
|
|
return s.writeCapsule(newAddressCapsule(capsuleTypeAddressAssign, rejections))
|
|
}
|
|
|
|
func (s *clientSession) handlePacket(buffer *buf.Buffer) {
|
|
_, destination, _, valid := packetAddresses(buffer.Bytes())
|
|
if !valid {
|
|
buffer.Release()
|
|
return
|
|
}
|
|
s.access.Lock()
|
|
configuration := s.configuration
|
|
s.access.Unlock()
|
|
if !prefixesContain(configuration.Address, destination) && !rangesContain(s.client.advertiseRoutes, destination) {
|
|
inet4Address, inet6Address := firstAddresses(configuration.Address)
|
|
reply, built := buildICMPError(buffer.Bytes(), tun.ICMPErrorNoRoute, inet4Address, inet6Address, 0, transportHTTP.CapsuleHeadroom)
|
|
buffer.Release()
|
|
if built {
|
|
_ = s.writePackets([]*buf.Buffer{reply})
|
|
}
|
|
return
|
|
}
|
|
err := s.client.handler.WriteInboundBuffers([]*buf.Buffer{buffer})
|
|
if err != nil {
|
|
s.client.logger.Debug(E.Cause(err, "write packet to device"))
|
|
}
|
|
}
|
|
|
|
func (s *clientSession) handlePacketTooBig(buffer *buf.Buffer, mtu int) {
|
|
s.access.Lock()
|
|
inet4Address, inet6Address := firstAddresses(s.configuration.Address)
|
|
s.access.Unlock()
|
|
reply, built := buildICMPError(buffer.Bytes(), tun.ICMPErrorPacketTooBig, inet4Address, inet6Address, mtu, s.client.handler.FrontHeadroom())
|
|
buffer.Release()
|
|
if !built {
|
|
return
|
|
}
|
|
err := s.client.handler.WriteInboundBuffers([]*buf.Buffer{reply})
|
|
if err != nil {
|
|
s.client.logger.Debug(E.Cause(err, "write packet to device"))
|
|
}
|
|
}
|
|
|
|
func firstAddresses(addresses []netip.Prefix) (netip.Addr, netip.Addr) {
|
|
var inet4Address netip.Addr
|
|
var inet6Address netip.Addr
|
|
for _, prefix := range addresses {
|
|
if prefix.Addr().Is4() && !inet4Address.IsValid() {
|
|
inet4Address = prefix.Addr()
|
|
} else if prefix.Addr().Is6() && !inet6Address.IsValid() {
|
|
inet6Address = prefix.Addr()
|
|
}
|
|
}
|
|
return inet4Address, inet6Address
|
|
}
|
|
|
|
func prefixesContain(prefixes []netip.Prefix, address netip.Addr) bool {
|
|
return slices.ContainsFunc(prefixes, func(prefix netip.Prefix) bool {
|
|
return prefix.Contains(address)
|
|
})
|
|
}
|
|
|
|
func RoutesContain(routes []AddressRange, address netip.Addr, protocol uint8) bool {
|
|
return slices.ContainsFunc(routes, func(route AddressRange) bool {
|
|
return route.Contains(address) && (route.Protocol == 0 || route.Protocol == protocol || isControlProtocol(protocol))
|
|
})
|
|
}
|
|
|
|
func rangesContain(routes []AddressRange, address netip.Addr) bool {
|
|
return slices.ContainsFunc(routes, func(route AddressRange) bool {
|
|
return route.Contains(address)
|
|
})
|
|
}
|