194 lines
4.8 KiB
Go
194 lines
4.8 KiB
Go
package ping
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-tun"
|
|
"github.com/sagernet/sing-tun/gtcpip/header"
|
|
"github.com/sagernet/sing/common/buf"
|
|
"github.com/sagernet/sing/common/control"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/logger"
|
|
)
|
|
|
|
const defaultFlowTimeout = time.Minute
|
|
|
|
type Port struct {
|
|
ctx context.Context
|
|
logger logger.ContextLogger
|
|
controlFunc func(destination netip.Addr) control.Func
|
|
timeout time.Duration
|
|
|
|
returnAccess sync.Mutex
|
|
returnPaths []tun.Return
|
|
|
|
flowAccess sync.Mutex
|
|
flows map[flowKey]*Destination
|
|
lastSweep time.Time
|
|
}
|
|
|
|
type flowKey struct {
|
|
source netip.Addr
|
|
destination netip.Addr
|
|
identifier uint16
|
|
}
|
|
|
|
func NewPort(ctx context.Context, logger logger.ContextLogger, controlFunc func(destination netip.Addr) control.Func, timeout time.Duration) *Port {
|
|
if timeout <= 0 {
|
|
timeout = defaultFlowTimeout
|
|
}
|
|
return &Port{
|
|
ctx: ctx,
|
|
logger: logger,
|
|
controlFunc: controlFunc,
|
|
timeout: timeout,
|
|
flows: make(map[flowKey]*Destination),
|
|
}
|
|
}
|
|
|
|
func (p *Port) PortAddresses() (netip.Addr, netip.Addr) {
|
|
return netip.IPv4Unspecified(), netip.IPv6Unspecified()
|
|
}
|
|
|
|
func (p *Port) PortMTU() uint32 {
|
|
return 0
|
|
}
|
|
|
|
func (p *Port) AttachReturn(returnPath tun.Return) error {
|
|
p.returnAccess.Lock()
|
|
defer p.returnAccess.Unlock()
|
|
if slices.Contains(p.returnPaths, returnPath) {
|
|
return nil
|
|
}
|
|
p.returnPaths = append(p.returnPaths[:len(p.returnPaths):len(p.returnPaths)], returnPath)
|
|
return nil
|
|
}
|
|
|
|
func (p *Port) DetachReturn(returnPath tun.Return) error {
|
|
p.returnAccess.Lock()
|
|
defer p.returnAccess.Unlock()
|
|
returnPaths := make([]tun.Return, 0, len(p.returnPaths))
|
|
for _, existing := range p.returnPaths {
|
|
if existing != returnPath {
|
|
returnPaths = append(returnPaths, existing)
|
|
}
|
|
}
|
|
p.returnPaths = returnPaths
|
|
return nil
|
|
}
|
|
|
|
func (p *Port) WritePackets(packets [][]byte) error {
|
|
var errs []error
|
|
for _, packet := range packets {
|
|
err := p.writePacket(packet)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return E.Errors(errs...)
|
|
}
|
|
|
|
func (p *Port) writePacket(packet []byte) error {
|
|
var (
|
|
source netip.Addr
|
|
destination netip.Addr
|
|
identifier uint16
|
|
)
|
|
switch header.IPVersion(packet) {
|
|
case header.IPv4Version:
|
|
ipHdr := header.IPv4(packet)
|
|
if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv4ProtocolNumber || ipHdr.PayloadLength() < header.ICMPv4MinimumSize {
|
|
return nil
|
|
}
|
|
icmpHdr := header.ICMPv4(ipHdr.Payload())
|
|
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 {
|
|
return nil
|
|
}
|
|
source = ipHdr.SourceAddr()
|
|
destination = ipHdr.DestinationAddr()
|
|
identifier = icmpHdr.Ident()
|
|
case header.IPv6Version:
|
|
ipHdr := header.IPv6(packet)
|
|
if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv6ProtocolNumber || ipHdr.PayloadLength() < header.ICMPv6MinimumSize {
|
|
return nil
|
|
}
|
|
icmpHdr := header.ICMPv6(ipHdr.Payload())
|
|
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 {
|
|
return nil
|
|
}
|
|
source = ipHdr.SourceAddr()
|
|
destination = ipHdr.DestinationAddr()
|
|
identifier = icmpHdr.Ident()
|
|
default:
|
|
return nil
|
|
}
|
|
flow, err := p.flowFor(source, destination, identifier)
|
|
if err != nil {
|
|
return E.Cause(err, "connect ICMP flow to ", destination)
|
|
}
|
|
return flow.WritePacket(buf.As(packet))
|
|
}
|
|
|
|
func (p *Port) flowFor(source netip.Addr, destination netip.Addr, identifier uint16) (*Destination, error) {
|
|
key := flowKey{source: source, destination: destination, identifier: identifier}
|
|
p.flowAccess.Lock()
|
|
defer p.flowAccess.Unlock()
|
|
now := time.Now()
|
|
if now.Sub(p.lastSweep) >= p.timeout {
|
|
p.lastSweep = now
|
|
for oldKey, oldFlow := range p.flows {
|
|
if oldFlow.IsClosed() {
|
|
delete(p.flows, oldKey)
|
|
}
|
|
}
|
|
}
|
|
flow, loaded := p.flows[key]
|
|
if loaded && !flow.IsClosed() {
|
|
return flow, nil
|
|
}
|
|
var controlFunc control.Func
|
|
if p.controlFunc != nil {
|
|
controlFunc = p.controlFunc(destination)
|
|
}
|
|
flow, err := ConnectDestination(p.ctx, p.logger, controlFunc, destination, portWriter{p}, p.timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.flows[key] = flow
|
|
return flow, nil
|
|
}
|
|
|
|
type portWriter struct {
|
|
port *Port
|
|
}
|
|
|
|
func (w portWriter) WritePacket(packet []byte) error {
|
|
w.port.returnAccess.Lock()
|
|
returnPaths := w.port.returnPaths
|
|
w.port.returnAccess.Unlock()
|
|
for _, returnPath := range returnPaths {
|
|
headroom := returnPath.ReturnHeadroom()
|
|
buffer := make([]byte, headroom+len(packet))
|
|
copy(buffer[headroom:], packet)
|
|
unconsumed := returnPath.ReturnPackets([][]byte{buffer})
|
|
if len(unconsumed) == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Port) Close() error {
|
|
p.flowAccess.Lock()
|
|
defer p.flowAccess.Unlock()
|
|
var errs []error
|
|
for key, flow := range p.flows {
|
|
errs = append(errs, flow.Close())
|
|
delete(p.flows, key)
|
|
}
|
|
return E.Errors(errs...)
|
|
}
|