diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7d5cea88..9de14e50 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,21 +16,56 @@ on: jobs: build: - name: Build + name: Lint ${{ matrix.goos }}/${{ matrix.goarch }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - goos: windows + goarch: amd64 + - goos: windows + goarch: '386' + - goos: windows + goarch: arm64 + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: linux + goarch: arm + - goos: linux + goarch: '386' + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: android + goarch: arm64 + - goos: freebsd + goarch: amd64 steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ^1.25 + - name: Cache go module + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + key: go-${{ hashFiles('**/go.sum') }} - name: golangci-lint uses: golangci/golangci-lint-action@v8 + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} with: version: latest args: --timeout=30m install-mode: binary - verify: false \ No newline at end of file + verify: false diff --git a/.golangci.yml b/.golangci.yml index 7a8a771d..0a8a526d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,35 +1,24 @@ version: "2" run: - go: "1.25" + go: "1.24" linters: default: none enable: - - govet - ineffassign - - paralleltest - staticcheck + - modernize settings: staticcheck: checks: - all - - -S1000 - - -S1008 - - -S1017 - - -ST1003 - - -QF1001 - - -QF1003 - - -QF1008 + - -QF1008 # could remove embedded field "" from selector + - -ST1003 # should not use ALL_CAPS in Go names; use CamelCase instead + - -QF1001 # could apply De Morgan's law exclusions: generated: lax presets: - comments - common-false-positives - - legacy - - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ formatters: enable: - gci @@ -42,8 +31,4 @@ formatters: - default custom-order: true exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ + generated: lax \ No newline at end of file diff --git a/Makefile b/Makefile index c7524749..ed36c792 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,10 @@ fmt_install: go install -v github.com/daixiang0/gci@latest lint: - GOOS=linux golangci-lint run . - GOOS=android golangci-lint run . - GOOS=windows golangci-lint run . - GOOS=darwin golangci-lint run . - GOOS=freebsd golangci-lint run . + GOOS=linux golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + GOOS=android golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + GOOS=windows golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + GOOS=darwin golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... lint_install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/flow.go b/flow.go new file mode 100644 index 00000000..1b1f34d6 --- /dev/null +++ b/flow.go @@ -0,0 +1,73 @@ +package tun + +import ( + "net/netip" + "time" +) + +type FlowVerdict struct { + Action FlowAction + Port Port + Destination netip.AddrPort + UDPTimeout time.Duration + NewTracker func() FlowTracker +} + +type FlowAction uint8 + +const ( + ActionAccept FlowAction = iota + ActionFlow + ActionReject + ActionDrop + ActionBypass +) + +type FlowTracker interface { + AttachFlow(handle FlowHandle) + CountForward(n int) + CountReverse(n int) + FlowEstablished() + CloseFlow(reason FlowCloseReason) +} + +type FlowHandle interface { + CloseFlow() +} + +type FlowCloseReason uint8 + +const ( + FlowCloseReset FlowCloseReason = iota + FlowCloseFinished + FlowCloseTimeout +) + +func (r FlowCloseReason) String() string { + switch r { + case FlowCloseFinished: + return "finished" + case FlowCloseTimeout: + return "idle timeout" + default: + return "connection reset" + } +} + +type Port interface { + PortAddresses() (v4 netip.Addr, v6 netip.Addr) + PortMTU() uint32 + AttachReturn(returnPath Return) error + DetachReturn(returnPath Return) error + WritePackets(packets [][]byte) error +} + +type PortWithSelectorRange interface { + Port + PortSelectorRange() (start uint16, count uint16) +} + +type Return interface { + ReturnHeadroom() int + ReturnPackets(packets [][]byte) [][]byte +} diff --git a/flow_dispatch.go b/flow_dispatch.go new file mode 100644 index 00000000..3f71751a --- /dev/null +++ b/flow_dispatch.go @@ -0,0 +1,791 @@ +package tun + +import ( + "maps" + "net/netip" + "sync/atomic" + "time" + + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +const ( + tcpEstablishedTimeout = 2*time.Hour + 4*time.Minute + tcpTransitoryTimeout = 4 * time.Minute + tcpClosingTimeout = 10 * time.Second + + defaultUDPTimeout = 5 * time.Minute + + defaultICMPTimeout = time.Minute + + flowTombstoneTimeout = 4 * time.Minute + + flowTableCapacity = 16384 + + flowSweepInterval = 30 * time.Second + flowSweepLimit = flowTableCapacity / int(flowTombstoneTimeout/flowSweepInterval) +) + +type ForwardWriteback interface { + ReturnHeadroom() int + WriteReturnPackets(packets [][]byte) error +} + +type flowEntry struct { + action FlowAction + deadline int64 + idle time.Duration + flow *forwardFlow +} + +type forwardFlow struct { + nat *portNAT + reverseKey flowKey + forwardRule rewriteRule + reverseRule rewriteRule + effectiveMTU uint32 + protocol uint8 + udpTimeout time.Duration + tracker FlowTracker + + clientAddress netip.Addr + clientSelector uint16 + clientDestinationAddress netip.Addr + clientDestinationPort uint16 + serverAddress netip.Addr + dnatAddress bool + dnatPort bool + + finForward atomic.Bool + established atomic.Bool + finReverse atomic.Bool + reported atomic.Bool + closed atomic.Bool + lastReverse atomic.Int64 +} + +func (f *forwardFlow) report(reason FlowCloseReason) { + if !f.reported.CompareAndSwap(false, true) { + return + } + if f.tracker != nil { + f.tracker.CloseFlow(reason) + } +} + +func (f *forwardFlow) close(reason FlowCloseReason) { + if !f.closed.CompareAndSwap(false, true) { + return + } + f.report(reason) +} + +func (f *forwardFlow) CloseFlow() { + f.close(FlowCloseReset) +} + +func (f *forwardFlow) observeReverse(packet *forwardPacket, now int64) { + f.lastReverse.Store(now) + if packet.protocol != uint8(header.TCPProtocolNumber) { + return + } + if f.established.CompareAndSwap(false, true) && f.tracker != nil { + f.tracker.FlowEstablished() + } + if packet.tcpFlags&header.TCPFlagRst != 0 { + f.close(FlowCloseReset) + return + } + if packet.tcpFlags&header.TCPFlagFin != 0 { + f.finReverse.Store(true) + } else if f.finReverse.Load() && f.finForward.Load() { + f.report(FlowCloseFinished) + } +} + +type ForwardDispatcher struct { + epoch time.Time + handler Handler + writeback ForwardWriteback + logger logger.Logger + udpTimeout time.Duration + icmpTimeout time.Duration + + table map[flowKey]*flowEntry + lastSweep int64 + ports map[Port]*portNAT + natList atomic.Pointer[[]*portNAT] + revNAT atomic.Pointer[map[netip.Addr]*portNAT] + + activeNATs []*portNAT + writebackBatch [][]byte + returnPath forwardReturn + exhaustedLogAt int64 + + segmentBuffers [][]byte + segmentSizes []int + segmentUsed int +} + +func addrToTCPIP(addr netip.Addr) tcpip.Address { + if addr.Is4() { + return tcpip.AddrFrom4(addr.As4()) + } + return tcpip.AddrFrom16(addr.As16()) +} + +func NewForwardDispatcher(handler Handler, writeback ForwardWriteback, logger logger.Logger, udpTimeout time.Duration, icmpTimeout time.Duration) *ForwardDispatcher { + dispatcher := &ForwardDispatcher{ + epoch: time.Now(), + handler: handler, + writeback: writeback, + logger: logger, + udpTimeout: udpTimeout, + icmpTimeout: icmpTimeout, + table: make(map[flowKey]*flowEntry), + ports: make(map[Port]*portNAT), + } + if dispatcher.udpTimeout <= 0 { + dispatcher.udpTimeout = defaultUDPTimeout + } + if dispatcher.icmpTimeout <= 0 { + dispatcher.icmpTimeout = defaultICMPTimeout + } + dispatcher.returnPath.dispatcher = dispatcher + return dispatcher +} + +func (d *ForwardDispatcher) now() int64 { + return int64(time.Since(d.epoch)) +} + +func (d *ForwardDispatcher) Close() { + if d == nil { + return + } + d.returnPath.closed.Store(true) + for _, entry := range d.table { + if entry.flow != nil { + entry.flow.close(FlowCloseReset) + } + } + for port, nat := range d.ports { + if nat != nil { + port.DetachReturn(&d.returnPath) + } + } +} + +func (d *ForwardDispatcher) Dispatch(packet []byte) bool { + if d == nil { + return false + } + parsed, ok := parseForwardPacket(packet) + if !ok || parsed.fragment || !parsed.hasFlow { + return false + } + key := parsed.flowKey() + now := d.now() + entry, loaded := d.table[key] + if loaded && d.entryExpired(entry, now) { + d.removeEntry(key, entry, FlowCloseTimeout) + loaded = false + } + if loaded { + return d.handleHit(key, entry, &parsed, packet, now) + } + if parsed.protocol == uint8(header.TCPProtocolNumber) && + (parsed.tcpFlags&header.TCPFlagSyn == 0 || parsed.tcpFlags&header.TCPFlagAck != 0) { + return false + } + return d.judgeAndInstall(key, &parsed, packet, now) +} + +func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *forwardPacket, raw []byte, now int64) bool { + switch entry.action { + case ActionFlow: + flow := entry.flow + if flow.closed.Load() { + d.tombstoneEntry(entry, now) + return true + } + var flowFinished bool + if packet.protocol == uint8(header.TCPProtocolNumber) { + if packet.tcpFlags&header.TCPFlagRst != 0 { + d.forwardToPort(flow, packet, raw) + flow.close(FlowCloseReset) + d.tombstoneEntry(entry, now) + return true + } + if packet.tcpFlags&header.TCPFlagFin != 0 { + flow.finForward.Store(true) + } else if flow.finForward.Load() && flow.finReverse.Load() { + flowFinished = true + } + } + entry.idle = d.flowIdle(flow) + entry.deadline = now + int64(entry.idle) + d.forwardToPort(flow, packet, raw) + if flowFinished { + flow.report(FlowCloseFinished) + } + return true + case ActionAccept: + if packet.protocol == uint8(header.TCPProtocolNumber) { + if packet.tcpFlags&header.TCPFlagRst != 0 { + d.removeEntry(key, entry, FlowCloseReset) + return false + } + if packet.tcpFlags&header.TCPFlagSyn == 0 { + entry.idle = tcpEstablishedTimeout + } + } + entry.deadline = now + int64(entry.idle) + return false + case ActionReject: + entry.deadline = now + int64(entry.idle) + d.stageReject(packet) + return true + default: + entry.deadline = now + int64(entry.idle) + return true + } +} + +func (d *ForwardDispatcher) judgeAndInstall(key flowKey, packet *forwardPacket, raw []byte, now int64) bool { + var firstPacket []byte + if packet.protocol == uint8(header.UDPProtocolNumber) { + firstPacket = header.UDP(packet.transport).Payload() + } + verdict := d.handler.JudgeFlow(packet.protocol, packet.source, packet.destination, firstPacket) + switch verdict.Action { + case ActionFlow: + if verdict.Port != nil { + flow, result := d.createFlow(packet, verdict) + if result == createFlowOK { + entry := &flowEntry{action: ActionFlow, flow: flow, idle: d.flowIdle(flow)} + entry.deadline = now + int64(entry.idle) + d.insertEntry(key, entry, now) + d.forwardToPort(flow, packet, raw) + return true + } + if result == createFlowExhausted { + if now-d.exhaustedLogAt >= int64(exhaustedLogInterval) { + d.exhaustedLogAt = now + d.logger.Warn("port selector range exhausted, rejecting flow to ", packet.destination) + } + d.installSimple(key, ActionReject, packet.protocol, now) + d.stageReject(packet) + return true + } + } + d.installSimple(key, ActionAccept, packet.protocol, now) + return false + case ActionReject: + d.installSimple(key, ActionReject, packet.protocol, now) + d.stageReject(packet) + return true + case ActionDrop: + d.installSimple(key, ActionDrop, packet.protocol, now) + return true + default: + d.installSimple(key, ActionAccept, packet.protocol, now) + return false + } +} + +func (d *ForwardDispatcher) installSimple(key flowKey, action FlowAction, protocol uint8, now int64) { + entry := &flowEntry{action: action, idle: d.idleTimeout(protocol, false)} + entry.deadline = now + int64(entry.idle) + d.insertEntry(key, entry, now) +} + +func (d *ForwardDispatcher) idleTimeout(protocol uint8, established bool) time.Duration { + switch protocol { + case uint8(header.TCPProtocolNumber): + if established { + return tcpEstablishedTimeout + } + return tcpTransitoryTimeout + case uint8(header.UDPProtocolNumber): + return d.udpTimeout + default: + return d.icmpTimeout + } +} + +func (d *ForwardDispatcher) flowIdle(flow *forwardFlow) time.Duration { + if flow.protocol == uint8(header.TCPProtocolNumber) && flow.finForward.Load() && flow.finReverse.Load() { + return tcpClosingTimeout + } + if flow.udpTimeout > 0 { + return flow.udpTimeout + } + established := flow.established.Load() && !flow.finForward.Load() && !flow.finReverse.Load() + return d.idleTimeout(flow.protocol, established) +} + +type createFlowResult uint8 + +const ( + createFlowOK createFlowResult = iota + createFlowUnsupported + createFlowExhausted +) + +const exhaustedLogInterval = 5 * time.Second + +func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdict) (*forwardFlow, createFlowResult) { + var portAddress netip.Addr + inet4Address, inet6Address := verdict.Port.PortAddresses() + if packet.ipVersion == 6 { + portAddress = inet6Address + } else { + portAddress = inet4Address + } + if !portAddress.IsValid() { + return nil, createFlowUnsupported + } + effectiveMTU := verdict.Port.PortMTU() + if packet.ipVersion == 6 && effectiveMTU != 0 && effectiveMTU < header.IPv6MinimumMTU { + return nil, createFlowUnsupported + } + isICMP := isICMPProtocol(packet.protocol) + clientDestinationAddress := packet.destination.Addr() + clientDestinationPort := packet.destination.Port() + serverAddress := clientDestinationAddress + serverPort := clientDestinationPort + if verdict.Destination.Addr().IsValid() { + serverAddress = verdict.Destination.Addr() + } + if verdict.Destination.Port() != 0 && !isICMP { + serverPort = verdict.Destination.Port() + } + nat := d.natFor(verdict.Port) + if nat == nil { + return nil, createFlowUnsupported + } + selector, reverseKey, allocated := nat.allocateSelector(packet.protocol, portAddress, serverAddress, serverPort, packet.source.Port()) + if !allocated { + return nil, createFlowExhausted + } + var udpTimeout time.Duration + if packet.protocol == uint8(header.UDPProtocolNumber) { + udpTimeout = verdict.UDPTimeout + } + flow := &forwardFlow{ + nat: nat, + reverseKey: reverseKey, + effectiveMTU: effectiveMTU, + protocol: packet.protocol, + udpTimeout: udpTimeout, + clientAddress: packet.source.Addr(), + clientSelector: packet.source.Port(), + clientDestinationAddress: clientDestinationAddress, + clientDestinationPort: clientDestinationPort, + serverAddress: serverAddress, + dnatAddress: serverAddress != clientDestinationAddress, + dnatPort: serverPort != clientDestinationPort && !isICMP, + } + flow.forwardRule = rewriteRule{ + sourceAddress: addrToTCPIP(portAddress), + sourcePort: selector, + rewriteSourcePort: true, + } + if flow.dnatAddress { + flow.forwardRule.destinationAddress = addrToTCPIP(serverAddress) + } + if flow.dnatPort { + flow.forwardRule.destinationPort = serverPort + flow.forwardRule.rewriteDestinationPort = true + } + flow.reverseRule = rewriteRule{ + destinationAddress: addrToTCPIP(flow.clientAddress), + destinationPort: flow.clientSelector, + rewriteDestinationPort: true, + } + if flow.dnatAddress { + flow.reverseRule.sourceAddress = addrToTCPIP(clientDestinationAddress) + } + if flow.dnatPort { + flow.reverseRule.sourcePort = clientDestinationPort + flow.reverseRule.rewriteSourcePort = true + } + if verdict.NewTracker != nil { + flow.tracker = verdict.NewTracker() + if flow.tracker != nil { + flow.tracker.AttachFlow(flow) + } + } + nat.insert(reverseKey, flow) + return flow, createFlowOK +} + +func (d *ForwardDispatcher) natFor(port Port) *portNAT { + nat, loaded := d.ports[port] + if loaded { + return nat + } + err := port.AttachReturn(&d.returnPath) + if err != nil { + d.logger.Trace(E.Cause(err, "attach return path")) + return nil + } + nat = newPortNAT(port) + d.ports[port] = nat + var natList []*portNAT + current := d.natList.Load() + if current != nil { + natList = append(natList, *current...) + } + natList = append(natList, nat) + d.natList.Store(&natList) + revMap := make(map[netip.Addr]*portNAT) + if currentRev := d.revNAT.Load(); currentRev != nil { + maps.Copy(revMap, *currentRev) + } + v4Address, v6Address := port.PortAddresses() + if v4Address.IsValid() { + revMap[v4Address] = nat + } + if v6Address.IsValid() { + revMap[v6Address] = nat + } + d.revNAT.Store(&revMap) + return nat +} + +func (d *ForwardDispatcher) forwardToPort(flow *forwardFlow, packet *forwardPacket, raw []byte) { + if flow.effectiveMTU != 0 && uint32(len(raw)) > flow.effectiveMTU { + if packet.protocol == uint8(header.TCPProtocolNumber) { + if flow.tracker != nil { + flow.tracker.CountForward(len(raw)) + } + d.rewriteForward(flow, packet) + d.resegmentTCP(flow, packet, raw) + return + } + if packet.ipVersion == 4 { + ipHdr := packet.network.(header.IPv4) + if ipHdr.Flags()&header.IPv4FlagDontFragment == 0 { + if flow.tracker != nil { + flow.tracker.CountForward(len(raw)) + } + d.rewriteForward(flow, packet) + fragments, ok := fragmentIPv4Packet(ipHdr, flow.effectiveMTU) + if ok { + for _, fragment := range fragments { + d.stagePort(flow.nat, fragment) + } + } + return + } + reply, ok := buildFragmentationNeeded(ipHdr, flow.effectiveMTU, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } + return + } + reply, ok := buildPacketTooBig(packet.network.(header.IPv6), flow.effectiveMTU, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } + return + } + if flow.tracker != nil { + flow.tracker.CountForward(len(raw)) + } + d.rewriteForward(flow, packet) + d.stagePort(flow.nat, raw) +} + +func (d *ForwardDispatcher) rewriteForward(flow *forwardFlow, packet *forwardPacket) { + if packet.isTCPSyn() { + applyRewriteRaw(packet, &flow.forwardRule) + clampTCPMSS(packet, flow.effectiveMTU) + recomputeChecksums(packet) + } else { + applyRewrite(packet, &flow.forwardRule) + } +} + +func (d *ForwardDispatcher) stagePort(nat *portNAT, packet []byte) { + if len(nat.pending) == 0 { + d.activeNATs = append(d.activeNATs, nat) + } + nat.pending = append(nat.pending, packet) +} + +func (d *ForwardDispatcher) flushPort(nat *portNAT) { + if len(nat.pending) == 0 { + return + } + err := nat.port.WritePackets(nat.pending) + if err != nil { + d.logger.Trace(E.Cause(err, "forward packets")) + } + nat.pending = nat.pending[:0] +} + +func (d *ForwardDispatcher) stageReject(packet *forwardPacket) { + reply, ok := buildReject(packet, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } +} + +func (d *ForwardDispatcher) Flush() { + if d == nil { + return + } + for _, nat := range d.activeNATs { + d.flushPort(nat) + } + d.activeNATs = d.activeNATs[:0] + if retain := max(d.segmentUsed, segmentRetainCount); len(d.segmentBuffers) > retain { + clear(d.segmentBuffers[retain:]) + d.segmentBuffers = d.segmentBuffers[:retain] + d.segmentSizes = d.segmentSizes[:retain] + } + d.segmentUsed = 0 + if len(d.writebackBatch) > 0 { + err := d.writeback.WriteReturnPackets(d.writebackBatch) + if err != nil { + d.logger.Trace(E.Cause(err, "write back packets")) + } + d.writebackBatch = d.writebackBatch[:0] + } + d.maybeSweep(d.now()) +} + +func (d *ForwardDispatcher) entryExpired(entry *flowEntry, now int64) bool { + if now <= entry.deadline { + return false + } + if entry.action == ActionFlow { + lastReverse := entry.flow.lastReverse.Load() + reverseDeadline := lastReverse + int64(entry.idle) + if lastReverse != 0 && now <= reverseDeadline { + entry.deadline = reverseDeadline + return false + } + } + return true +} + +func (d *ForwardDispatcher) tombstoneEntry(entry *flowEntry, now int64) { + entry.action = ActionDrop + entry.idle = flowTombstoneTimeout + entry.deadline = now + int64(entry.idle) +} + +func (d *ForwardDispatcher) removeEntry(key flowKey, entry *flowEntry, reason FlowCloseReason) { + delete(d.table, key) + if entry.flow != nil { + if reason == FlowCloseTimeout && entry.flow.finForward.Load() && entry.flow.finReverse.Load() { + reason = FlowCloseFinished + } + entry.flow.close(reason) + entry.flow.nat.delete(entry.flow.reverseKey) + } +} + +func (d *ForwardDispatcher) insertEntry(key flowKey, entry *flowEntry, now int64) { + if len(d.table) >= flowTableCapacity { + d.evictEntries(now) + } + d.table[key] = entry +} + +func (d *ForwardDispatcher) evictEntries(now int64) { + var ( + freed int + visited int + oldestKey flowKey + oldest *flowEntry + ) + for key, entry := range d.table { + if d.entryExpired(entry, now) { + d.removeEntry(key, entry, FlowCloseTimeout) + freed++ + } else if oldest == nil || entry.deadline < oldest.deadline { + oldestKey = key + oldest = entry + } + visited++ + if visited >= flowSweepLimit { + break + } + } + if freed == 0 && oldest != nil { + d.removeEntry(oldestKey, oldest, FlowCloseReset) + } +} + +func (d *ForwardDispatcher) maybeSweep(now int64) { + if now-d.lastSweep < int64(flowSweepInterval) { + return + } + d.lastSweep = now + visited := 0 + for key, entry := range d.table { + if entry.action == ActionFlow && entry.flow.closed.Load() { + d.tombstoneEntry(entry, now) + } else if d.entryExpired(entry, now) { + d.removeEntry(key, entry, FlowCloseTimeout) + } + visited++ + if visited >= flowSweepLimit { + break + } + } +} + +func isICMPProtocol(protocol uint8) bool { + return protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber) +} + +var _ Return = (*forwardReturn)(nil) + +type forwardReturn struct { + dispatcher *ForwardDispatcher + closed atomic.Bool +} + +func (r *forwardReturn) ReturnHeadroom() int { + return r.dispatcher.writeback.ReturnHeadroom() +} + +type returnDecision uint8 + +const ( + returnPass returnDecision = iota + returnWrite + returnDrop +) + +func (r *forwardReturn) ReturnPackets(packets [][]byte) [][]byte { + if r.closed.Load() { + return packets + } + natListPtr := r.dispatcher.natList.Load() + if natListPtr == nil { + return packets + } + natList := *natListPtr + var revMap map[netip.Addr]*portNAT + if revPtr := r.dispatcher.revNAT.Load(); revPtr != nil { + revMap = *revPtr + } + headroom := r.dispatcher.writeback.ReturnHeadroom() + now := r.dispatcher.now() + + if len(packets) == 1 { + switch r.classifyReturn(packets[0], natList, revMap, headroom, now) { + case returnWrite: + if err := r.dispatcher.writeback.WriteReturnPackets(packets[:1]); err != nil { + r.dispatcher.logger.Trace(E.Cause(err, "write return packets")) + } + return packets[:0] + case returnDrop: + return packets[:0] + default: + return packets + } + } + + unconsumed := packets[:0] + var writeBatch [][]byte + for _, raw := range packets { + switch r.classifyReturn(raw, natList, revMap, headroom, now) { + case returnWrite: + writeBatch = append(writeBatch, raw) + case returnDrop: + default: + unconsumed = append(unconsumed, raw) + } + } + if len(writeBatch) > 0 { + if err := r.dispatcher.writeback.WriteReturnPackets(writeBatch); err != nil { + r.dispatcher.logger.Trace(E.Cause(err, "write return packets")) + } + } + return unconsumed +} + +func (r *forwardReturn) classifyReturn(raw []byte, natList []*portNAT, revMap map[netip.Addr]*portNAT, headroom int, now int64) returnDecision { + if len(raw) < headroom+header.IPv4MinimumSize { + return returnPass + } + parsed, ok := parseForwardPacket(raw[headroom:]) + if !ok || parsed.fragment { + return returnPass + } + if !parsed.hasFlow { + if parsed.isICMPError() && returnICMPError(natList, revMap, &parsed) { + return returnWrite + } + return returnPass + } + flow := findReverseFlow(natList, revMap, parsed.flowKey()) + if flow == nil { + return returnPass + } + if flow.closed.Load() { + return returnDrop + } + if flow.tracker != nil { + flow.tracker.CountReverse(len(raw) - headroom) + } + flow.observeReverse(&parsed, now) + if parsed.isTCPSyn() { + applyRewriteRaw(&parsed, &flow.reverseRule) + clampTCPMSS(&parsed, flow.effectiveMTU) + recomputeChecksums(&parsed) + } else { + applyRewrite(&parsed, &flow.reverseRule) + } + return returnWrite +} + +func findReverseFlow(natList []*portNAT, revMap map[netip.Addr]*portNAT, key flowKey) *forwardFlow { + if nat, ok := revMap[key.destination.Addr()]; ok { + if flow := nat.lookup(key); flow != nil { + return flow + } + } + for _, nat := range natList { + if flow := nat.lookup(key); flow != nil { + return flow + } + } + return nil +} + +func returnICMPError(natList []*portNAT, revMap map[netip.Addr]*portNAT, parsed *forwardPacket) bool { + inner, ok := parsed.icmpErrorInner() + if !ok { + return false + } + embedded, parsedInner := parseEmbedded(inner) + if !parsedInner { + return false + } + flow := findReverseFlow(natList, revMap, embedded.flowKey().reversed()) + if flow == nil || flow.closed.Load() { + return false + } + rewriteEmbeddedSource(&embedded, addrToTCPIP(flow.clientAddress), flow.clientSelector, true) + if flow.dnatAddress || flow.dnatPort { + rewriteEmbeddedDestination(&embedded, addrToTCPIP(flow.clientDestinationAddress), flow.clientDestinationPort, flow.dnatPort) + } + parsed.network.SetDestinationAddr(flow.clientAddress) + if parsed.network.SourceAddr() == flow.serverAddress { + parsed.network.SetSourceAddr(flow.clientDestinationAddress) + } + recomputeChecksums(parsed) + return true +} diff --git a/flow_mtu.go b/flow_mtu.go new file mode 100644 index 00000000..6ce275b3 --- /dev/null +++ b/flow_mtu.go @@ -0,0 +1,167 @@ +package tun + +import ( + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" +) + +// segmentRetainCount bounds how many segment buffers survive a Flush; the pool +// grows to the burst high-water mark within a batch and is trimmed afterwards. +const segmentRetainCount = 128 + +// Linux delivers TSO aggregates to the TUN even with IFF_VNET_HDR off +// (observed on 6.x: the pre-segmentation skb is handed to the fd as-is). +func (d *ForwardDispatcher) resegmentTCP(flow *forwardFlow, packet *forwardPacket, raw []byte) { + if len(packet.transport) < header.TCPMinimumSize { + return + } + headerLength := len(raw) - len(packet.transport) + if packet.ipVersion == 6 && headerLength != header.IPv6MinimumSize { + reply, ok := buildPacketTooBig(packet.network.(header.IPv6), flow.effectiveMTU, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } + return + } + tcpHeaderLength := int(header.TCP(packet.transport).DataOffset()) + if tcpHeaderLength < header.TCPMinimumSize || tcpHeaderLength > len(packet.transport) { + return + } + totalHeaderLength := headerLength + tcpHeaderLength + segmentSize := int(flow.effectiveMTU) - totalHeaderLength + if segmentSize <= 0 { + return + } + gsoType := GSOTCPv4 + if packet.ipVersion == 6 { + gsoType = GSOTCPv6 + } + neededSegments := max((len(raw)-totalHeaderLength+segmentSize-1)/segmentSize, 1) + bufs, sizes := d.reserveSegments(neededSegments, int(flow.effectiveMTU)) + n, err := GSOSplit(raw, GSOOptions{ + GSOType: gsoType, + HdrLen: uint16(totalHeaderLength), + CsumStart: uint16(headerLength), + CsumOffset: header.TCPChecksumOffset, + GSOSize: uint16(segmentSize), + }, bufs, sizes, 0) + if err != nil { + d.logger.Trace(E.Cause(err, "resegment packet")) + return + } + for i := range n { + d.stagePort(flow.nat, bufs[i][:sizes[i]]) + } +} + +func (d *ForwardDispatcher) reserveSegments(count, size int) ([][]byte, []int) { + start := d.segmentUsed + end := start + count + for len(d.segmentBuffers) < end { + d.segmentBuffers = append(d.segmentBuffers, make([]byte, size)) + d.segmentSizes = append(d.segmentSizes, 0) + } + for i := start; i < end; i++ { + if cap(d.segmentBuffers[i]) < size { + d.segmentBuffers[i] = make([]byte, size) + } else { + d.segmentBuffers[i] = d.segmentBuffers[i][:size] + } + } + d.segmentUsed = end + return d.segmentBuffers[start:end], d.segmentSizes[start:end] +} + +const synthesizedTTL = 64 + +func fragmentIPv4Packet(packet header.IPv4, effectiveMTU uint32) ([][]byte, bool) { + headerLength := int(packet.HeaderLength()) + if headerLength < header.IPv4MinimumSize || headerLength >= len(packet) { + return nil, false + } + payload := packet[headerLength:] + maxFragmentPayload := (int(effectiveMTU) - headerLength) &^ 7 + if maxFragmentPayload <= 0 { + return nil, false + } + baseOffset := packet.FragmentOffset() + originalMore := packet.Flags()&header.IPv4FlagMoreFragments != 0 + baseFlags := packet.Flags() &^ header.IPv4FlagMoreFragments + fragments := make([][]byte, 0, (len(payload)+maxFragmentPayload-1)/maxFragmentPayload) + for start := 0; start < len(payload); start += maxFragmentPayload { + end := min(start+maxFragmentPayload, len(payload)) + fragment := header.IPv4(make([]byte, headerLength+end-start)) + copy(fragment, packet[:headerLength]) + copy(fragment[headerLength:], payload[start:end]) + flags := baseFlags + if originalMore || end < len(payload) { + flags |= header.IPv4FlagMoreFragments + } + fragment.SetFlagsFragmentOffset(flags, baseOffset+uint16(start)) + fragment.SetTotalLength(uint16(len(fragment))) + fragment.SetChecksum(0) + fragment.SetChecksum(^fragment.CalculateChecksum()) + fragments = append(fragments, fragment) + } + return fragments, true +} + +func buildFragmentationNeeded(packet header.IPv4, effectiveMTU uint32, headroom int) ([]byte, bool) { + advertised := max(effectiveMTU, header.IPv4MinimumMTU) + originalLength := min(int(packet.TotalLength()), len(packet)) + minPayloadLength := int(packet.HeaderLength()) + header.ICMPv4MinimumErrorPayloadSize + if originalLength < minPayloadLength { + return nil, false + } + maxPayloadLength := header.IPv4MinimumProcessableDatagramSize - header.IPv4MinimumSize - header.ICMPv4MinimumSize + payloadLength := min(originalLength, maxPayloadLength) + size := header.IPv4MinimumSize + header.ICMPv4MinimumSize + payloadLength + buffer := make([]byte, headroom+size) + response := header.IPv4(buffer[headroom:]) + response.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: packet.DestinationAddr(), + DstAddr: packet.SourceAddr(), + }) + response.SetChecksum(^response.CalculateChecksum()) + icmpHdr := header.ICMPv4(response.Payload()) + icmpHdr.SetType(header.ICMPv4DstUnreachable) + icmpHdr.SetCode(header.ICMPv4FragmentationNeeded) + icmpHdr.SetMTU(uint16(min(advertised, uint32(0xffff)))) + copy(icmpHdr.Payload(), packet[:payloadLength]) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + return buffer, true +} + +func buildPacketTooBig(packet header.IPv6, effectiveMTU uint32, headroom int) ([]byte, bool) { + advertised := max(effectiveMTU, header.IPv6MinimumMTU) + originalLength := min(header.IPv6MinimumSize+int(packet.PayloadLength()), len(packet)) + if originalLength < header.IPv6MinimumSize { + return nil, false + } + maxPayloadLength := header.IPv6MinimumMTU - header.IPv6MinimumSize - header.ICMPv6PacketTooBigMinimumSize + payloadLength := min(originalLength, maxPayloadLength) + size := header.IPv6MinimumSize + header.ICMPv6PacketTooBigMinimumSize + payloadLength + buffer := make([]byte, headroom+size) + response := header.IPv6(buffer[headroom:]) + response.Encode(&header.IPv6Fields{ + PayloadLength: uint16(header.ICMPv6PacketTooBigMinimumSize + payloadLength), + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: packet.DestinationAddr(), + DstAddr: packet.SourceAddr(), + }) + icmpHdr := header.ICMPv6(response.Payload()) + icmpHdr.SetType(header.ICMPv6PacketTooBig) + icmpHdr.SetCode(header.ICMPv6UnusedCode) + icmpHdr.SetMTU(advertised) + copy(icmpHdr.Payload(), packet[:payloadLength]) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: response.SourceAddressSlice(), + Dst: response.DestinationAddressSlice(), + })) + return buffer, true +} diff --git a/flow_nat.go b/flow_nat.go new file mode 100644 index 00000000..4d2bca3b --- /dev/null +++ b/flow_nat.go @@ -0,0 +1,121 @@ +package tun + +import ( + "net/netip" + "runtime" + "sync" + + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/contrab/maphash" +) + +const ( + natSelectorMin = 49152 + natSelectorMax = 65535 +) + +type portNAT struct { + port Port + hasher maphash.Hasher[flowKey] + shardMask uint32 + shards []natShard + selectorStart uint16 + selectorCount uint16 + + counter uint32 + pending [][]byte +} + +type natShard struct { + access sync.RWMutex + flows map[flowKey]*forwardFlow +} + +func newPortNAT(port Port) *portNAT { + shardCount := 1 + for shardCount < runtime.GOMAXPROCS(0) { + shardCount <<= 1 + } + nat := &portNAT{ + port: port, + hasher: maphash.NewHasher[flowKey](), + shardMask: uint32(shardCount - 1), + shards: make([]natShard, shardCount), + } + if rangedPort, isRanged := port.(PortWithSelectorRange); isRanged { + nat.selectorStart, nat.selectorCount = rangedPort.PortSelectorRange() + } + for i := range nat.shards { + nat.shards[i].flows = make(map[flowKey]*forwardFlow) + } + return nat +} + +func (n *portNAT) shard(key flowKey) *natShard { + return &n.shards[n.hasher.Hash32(key)&n.shardMask] +} + +func (n *portNAT) lookup(key flowKey) *forwardFlow { + shard := n.shard(key) + shard.access.RLock() + flow := shard.flows[key] + shard.access.RUnlock() + return flow +} + +func (n *portNAT) insert(key flowKey, flow *forwardFlow) { + shard := n.shard(key) + shard.access.Lock() + shard.flows[key] = flow + shard.access.Unlock() +} + +func (n *portNAT) delete(key flowKey) { + shard := n.shard(key) + shard.access.Lock() + delete(shard.flows, key) + shard.access.Unlock() +} + +func (n *portNAT) reverseKeyFor(protocol uint8, portAddress, serverAddress netip.Addr, serverPort, selector uint16) flowKey { + if protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber) { + return flowKey{ + protocol: protocol, + source: netip.AddrPortFrom(serverAddress, selector), + destination: netip.AddrPortFrom(portAddress, selector), + } + } + return flowKey{ + protocol: protocol, + source: netip.AddrPortFrom(serverAddress, serverPort), + destination: netip.AddrPortFrom(portAddress, selector), + } +} + +func (n *portNAT) selectorRange(protocol uint8) (uint16, uint32) { + if n.selectorCount == 0 || + protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber) { + return natSelectorMin, natSelectorMax - natSelectorMin + 1 + } + return n.selectorStart, uint32(n.selectorCount) +} + +func (n *portNAT) allocateSelector(protocol uint8, portAddress, serverAddress netip.Addr, serverPort, clientSelector uint16) (uint16, flowKey, bool) { + rangeStart, rangeCount := n.selectorRange(protocol) + if clientSelector != 0 && + clientSelector >= rangeStart && uint32(clientSelector-rangeStart) < rangeCount { + key := n.reverseKeyFor(protocol, portAddress, serverAddress, serverPort, clientSelector) + if n.lookup(key) == nil { + return clientSelector, key, true + } + } + for range rangeCount { + n.counter++ + candidate := rangeStart + uint16(n.counter%rangeCount) + key := n.reverseKeyFor(protocol, portAddress, serverAddress, serverPort, candidate) + if n.lookup(key) == nil { + return candidate, key, true + } + } + return 0, flowKey{}, false +} diff --git a/flow_parse.go b/flow_parse.go new file mode 100644 index 00000000..45887994 --- /dev/null +++ b/flow_parse.go @@ -0,0 +1,256 @@ +package tun + +import ( + "encoding/binary" + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/header" +) + +type flowKey struct { + protocol uint8 + source netip.AddrPort + destination netip.AddrPort +} + +func (k flowKey) reversed() flowKey { + return flowKey{protocol: k.protocol, source: k.destination, destination: k.source} +} + +type forwardPacket struct { + ipVersion uint8 + protocol uint8 + network header.Network + transport []byte + source netip.AddrPort + destination netip.AddrPort + tcpFlags header.TCPFlags + icmpType uint8 + fragment bool + hasFlow bool +} + +func (p *forwardPacket) flowKey() flowKey { + return flowKey{protocol: p.protocol, source: p.source, destination: p.destination} +} + +func (p *forwardPacket) isTCPSyn() bool { + return p.protocol == uint8(header.TCPProtocolNumber) && p.tcpFlags&header.TCPFlagSyn != 0 +} + +func parseForwardPacket(packet []byte) (forwardPacket, bool) { + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) { + return forwardPacket{}, false + } + parsed := forwardPacket{ + ipVersion: 4, + protocol: uint8(ipHdr.TransportProtocol()), + network: ipHdr, + source: netip.AddrPortFrom(ipHdr.SourceAddr(), 0), + destination: netip.AddrPortFrom(ipHdr.DestinationAddr(), 0), + } + if ipHdr.More() || ipHdr.FragmentOffset() != 0 { + parsed.fragment = true + return parsed, true + } + parsed.parseTransport(ipHdr.Payload()) + return parsed, true + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) { + return forwardPacket{}, false + } + protocol, payload, fragment, transportPresent := skipIPv6ExtensionHeaders(uint8(ipHdr.TransportProtocol()), ipHdr.Payload()) + parsed := forwardPacket{ + ipVersion: 6, + protocol: protocol, + network: ipHdr, + source: netip.AddrPortFrom(ipHdr.SourceAddr(), 0), + destination: netip.AddrPortFrom(ipHdr.DestinationAddr(), 0), + fragment: fragment, + } + if fragment || !transportPresent { + return parsed, true + } + parsed.parseTransport(payload) + return parsed, true + default: + return forwardPacket{}, false + } +} + +func skipIPv6ExtensionHeaders(protocol uint8, payload []byte) (uint8, []byte, bool, bool) { + for { + switch header.IPv6ExtensionHeaderIdentifier(protocol) { + case header.IPv6HopByHopOptionsExtHdrIdentifier, header.IPv6RoutingExtHdrIdentifier, header.IPv6DestinationOptionsExtHdrIdentifier: + if len(payload) < 2 { + return protocol, payload, false, false + } + extensionLength := (int(payload[1]) + 1) * 8 + if len(payload) < extensionLength { + return protocol, payload, false, false + } + protocol = payload[0] + payload = payload[extensionLength:] + case header.IPv6FragmentExtHdrIdentifier: + return protocol, payload, true, false + default: + return protocol, payload, false, true + } + } +} + +func (p *forwardPacket) parseTransport(payload []byte) { + p.transport = payload + switch p.protocol { + case uint8(header.TCPProtocolNumber): + if len(payload) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(payload) + p.source = netip.AddrPortFrom(p.source.Addr(), tcpHdr.SourcePort()) + p.destination = netip.AddrPortFrom(p.destination.Addr(), tcpHdr.DestinationPort()) + p.tcpFlags = tcpHdr.Flags() + p.hasFlow = true + case uint8(header.UDPProtocolNumber): + if len(payload) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(payload) + p.source = netip.AddrPortFrom(p.source.Addr(), udpHdr.SourcePort()) + p.destination = netip.AddrPortFrom(p.destination.Addr(), udpHdr.DestinationPort()) + p.hasFlow = true + case uint8(header.ICMPv4ProtocolNumber): + if len(payload) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(payload) + p.icmpType = uint8(icmpHdr.Type()) + switch icmpHdr.Type() { + case header.ICMPv4Echo, header.ICMPv4EchoReply: + identifier := icmpHdr.Ident() + p.source = netip.AddrPortFrom(p.source.Addr(), identifier) + p.destination = netip.AddrPortFrom(p.destination.Addr(), identifier) + p.hasFlow = true + } + case uint8(header.ICMPv6ProtocolNumber): + if len(payload) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(payload) + p.icmpType = uint8(icmpHdr.Type()) + switch icmpHdr.Type() { + case header.ICMPv6EchoRequest, header.ICMPv6EchoReply: + identifier := icmpHdr.Ident() + p.source = netip.AddrPortFrom(p.source.Addr(), identifier) + p.destination = netip.AddrPortFrom(p.destination.Addr(), identifier) + p.hasFlow = true + } + } +} + +func (p *forwardPacket) isICMPError() bool { + switch p.protocol { + case uint8(header.ICMPv4ProtocolNumber): + switch header.ICMPv4Type(p.icmpType) { + case header.ICMPv4DstUnreachable, header.ICMPv4SrcQuench, header.ICMPv4Redirect, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem: + return true + } + return false + case uint8(header.ICMPv6ProtocolNumber): + return header.ICMPv6Type(p.icmpType).IsErrorType() + default: + return false + } +} + +func (p *forwardPacket) icmpErrorInner() ([]byte, bool) { + var innerOffset int + switch p.protocol { + case uint8(header.ICMPv4ProtocolNumber): + innerOffset = header.ICMPv4MinimumSize + case uint8(header.ICMPv6ProtocolNumber): + innerOffset = header.ICMPv6ErrorHeaderSize + default: + return nil, false + } + if len(p.transport) <= innerOffset { + return nil, false + } + return p.transport[innerOffset:], true +} + +type embeddedPacket struct { + network header.Network + payload []byte + protocol uint8 + source netip.AddrPort + destination netip.AddrPort +} + +func (p *embeddedPacket) flowKey() flowKey { + return flowKey{protocol: p.protocol, source: p.source, destination: p.destination} +} + +func parseEmbedded(inner []byte) (embeddedPacket, bool) { + switch header.IPVersion(inner) { + case header.IPv4Version: + if len(inner) < header.IPv4MinimumSize { + return embeddedPacket{}, false + } + ipHdr := header.IPv4(inner) + headerLength := int(ipHdr.HeaderLength()) + if headerLength < header.IPv4MinimumSize || headerLength > len(inner) { + return embeddedPacket{}, false + } + return parseEmbeddedTransport(ipHdr, inner[headerLength:], uint8(ipHdr.TransportProtocol()), ipHdr.SourceAddr(), ipHdr.DestinationAddr()) + case header.IPv6Version: + if len(inner) < header.IPv6MinimumSize { + return embeddedPacket{}, false + } + ipHdr := header.IPv6(inner) + protocol, payload, _, transportPresent := skipIPv6ExtensionHeaders(uint8(ipHdr.TransportProtocol()), inner[header.IPv6MinimumSize:]) + if !transportPresent { + return embeddedPacket{}, false + } + return parseEmbeddedTransport(ipHdr, payload, protocol, ipHdr.SourceAddr(), ipHdr.DestinationAddr()) + default: + return embeddedPacket{}, false + } +} + +func parseEmbeddedTransport(network header.Network, payload []byte, protocol uint8, source, destination netip.Addr) (embeddedPacket, bool) { + embedded := embeddedPacket{ + network: network, + payload: payload, + protocol: protocol, + } + switch protocol { + case uint8(header.TCPProtocolNumber), uint8(header.UDPProtocolNumber): + if len(payload) < 4 { + return embeddedPacket{}, false + } + embedded.source = netip.AddrPortFrom(source, binary.BigEndian.Uint16(payload[0:])) + embedded.destination = netip.AddrPortFrom(destination, binary.BigEndian.Uint16(payload[2:])) + case uint8(header.ICMPv4ProtocolNumber): + if len(payload) < header.ICMPv4MinimumSize { + return embeddedPacket{}, false + } + identifier := header.ICMPv4(payload).Ident() + embedded.source = netip.AddrPortFrom(source, identifier) + embedded.destination = netip.AddrPortFrom(destination, identifier) + case uint8(header.ICMPv6ProtocolNumber): + if len(payload) < header.ICMPv6MinimumSize { + return embeddedPacket{}, false + } + identifier := header.ICMPv6(payload).Ident() + embedded.source = netip.AddrPortFrom(source, identifier) + embedded.destination = netip.AddrPortFrom(destination, identifier) + default: + return embeddedPacket{}, false + } + return embedded, true +} diff --git a/flow_reject.go b/flow_reject.go new file mode 100644 index 00000000..af82eb5c --- /dev/null +++ b/flow_reject.go @@ -0,0 +1,210 @@ +package tun + +import ( + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" +) + +func buildReject(packet *forwardPacket, headroom int) ([]byte, bool) { + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(packet.transport) < header.TCPMinimumSize { + return nil, false + } + tcpHdr := header.TCP(packet.transport) + switch ipHdr := packet.network.(type) { + case header.IPv4: + return buildResetIPv4(ipHdr, tcpHdr, headroom), true + case header.IPv6: + return buildResetIPv6(ipHdr, tcpHdr, headroom), true + default: + return nil, false + } + case uint8(header.UDPProtocolNumber): + switch ipHdr := packet.network.(type) { + case header.IPv4: + return buildRejectICMPv4(ipHdr, header.ICMPv4PortUnreachable, ipHdr.DestinationAddr(), headroom) + case header.IPv6: + return buildRejectICMPv6(ipHdr, header.ICMPv6PortUnreachable, ipHdr.DestinationAddr(), headroom) + default: + return nil, false + } + default: + switch ipHdr := packet.network.(type) { + case header.IPv4: + return buildRejectICMPv4(ipHdr, header.ICMPv4HostUnreachable, ipHdr.DestinationAddr(), headroom) + case header.IPv6: + return buildRejectICMPv6(ipHdr, header.ICMPv6AddressUnreachable, ipHdr.DestinationAddr(), headroom) + default: + return nil, false + } + } +} + +func buildResetIPv4(origIPHdr header.IPv4, origTCPHdr header.TCP, headroom int) []byte { + size := header.IPv4MinimumSize + header.TCPMinimumSize + buffer := make([]byte, headroom+size) + ipHdr := header.IPv4(buffer[headroom:]) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.TCPProtocolNumber), + SrcAddr: origIPHdr.DestinationAddr(), + DstAddr: origIPHdr.SourceAddr(), + }) + tcpHdr := header.TCP(ipHdr.Payload()) + encodeResetTCP(tcpHdr, origTCPHdr) + tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + return buffer +} + +func buildResetIPv6(origIPHdr header.IPv6, origTCPHdr header.TCP, headroom int) []byte { + size := header.IPv6MinimumSize + header.TCPMinimumSize + buffer := make([]byte, headroom+size) + ipHdr := header.IPv6(buffer[headroom:]) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(header.TCPMinimumSize), + TransportProtocol: header.TCPProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: origIPHdr.DestinationAddr(), + DstAddr: origIPHdr.SourceAddr(), + }) + tcpHdr := header.TCP(ipHdr.Payload()) + encodeResetTCP(tcpHdr, origTCPHdr) + tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) + return buffer +} + +func encodeResetTCP(tcpHdr header.TCP, origTCPHdr header.TCP) { + fields := header.TCPFields{ + SrcPort: origTCPHdr.DestinationPort(), + DstPort: origTCPHdr.SourcePort(), + DataOffset: header.TCPMinimumSize, + Flags: header.TCPFlagRst, + } + if origTCPHdr.Flags()&header.TCPFlagAck != 0 { + fields.SeqNum = origTCPHdr.AckNumber() + } else { + fields.Flags |= header.TCPFlagAck + ackNumber := origTCPHdr.SequenceNumber() + uint32(len(origTCPHdr.Payload())) + if origTCPHdr.Flags()&header.TCPFlagSyn != 0 { + ackNumber++ + } + if origTCPHdr.Flags()&header.TCPFlagFin != 0 { + ackNumber++ + } + fields.AckNum = ackNumber + } + tcpHdr.Encode(&fields) +} + +func buildRejectICMPv4(ipHdr header.IPv4, code header.ICMPv4Code, source netip.Addr, headroom int) ([]byte, bool) { + const maxIPData = header.IPv4MinimumProcessableDatagramSize - header.IPv4MinimumSize + available := maxIPData - header.ICMPv4MinimumSize + if len(ipHdr) < header.ICMPv4MinimumErrorPayloadSize { + return nil, false + } + payload := []byte(ipHdr) + if len(payload) > available { + payload = payload[:available] + } + size := header.IPv4MinimumSize + header.ICMPv4MinimumSize + len(payload) + buffer := make([]byte, headroom+size) + newIPHdr := header.IPv4(buffer[headroom:]) + newIPHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: source, + DstAddr: ipHdr.SourceAddr(), + }) + newIPHdr.SetChecksum(^newIPHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(newIPHdr.Payload()) + icmpHdr.SetType(header.ICMPv4DstUnreachable) + icmpHdr.SetCode(code) + copy(icmpHdr.Payload(), payload) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(payload, 0))) + return buffer, true +} + +func buildRejectICMPv6(ipHdr header.IPv6, code header.ICMPv6Code, source netip.Addr, headroom int) ([]byte, bool) { + const maxIPv6Data = header.IPv6MinimumMTU - header.IPv6FixedHeaderSize + available := maxIPv6Data - header.ICMPv6ErrorHeaderSize + if available < header.IPv6MinimumSize { + return nil, false + } + payload := []byte(ipHdr) + if len(payload) > available { + payload = payload[:available] + } + size := header.IPv6MinimumSize + header.ICMPv6DstUnreachableMinimumSize + len(payload) + buffer := make([]byte, headroom+size) + newIPHdr := header.IPv6(buffer[headroom:]) + newIPHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(header.ICMPv6DstUnreachableMinimumSize + len(payload)), + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: source, + DstAddr: ipHdr.SourceAddr(), + }) + icmpHdr := header.ICMPv6(newIPHdr.Payload()) + icmpHdr.SetType(header.ICMPv6DstUnreachable) + icmpHdr.SetCode(code) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], + Src: newIPHdr.SourceAddressSlice(), + Dst: newIPHdr.DestinationAddressSlice(), + PayloadCsum: checksum.Checksum(payload, 0), + PayloadLen: len(payload), + })) + copy(icmpHdr.Payload(), payload) + return buffer, true +} + +func BuildUnreachable(packet []byte, source netip.Addr, headroom int) ([]byte, bool) { + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.FragmentOffset() != 0 { + return nil, false + } + sourceAddr := ipHdr.SourceAddr() + if sourceAddr.IsUnspecified() || sourceAddr.IsMulticast() { + return nil, false + } + if ipHdr.TransportProtocol() == header.ICMPv4ProtocolNumber { + if len(ipHdr.Payload()) < header.ICMPv4MinimumSize || header.ICMPv4(ipHdr.Payload()).Type() != header.ICMPv4Echo { + return nil, false + } + } + replySource := ipHdr.DestinationAddr() + if source.Is4() { + replySource = source + } + return buildRejectICMPv4(ipHdr, header.ICMPv4HostUnreachable, replySource, headroom) + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) { + return nil, false + } + sourceAddr := ipHdr.SourceAddr() + if sourceAddr.IsUnspecified() || sourceAddr.IsMulticast() { + return nil, false + } + if ipHdr.TransportProtocol() == header.ICMPv6ProtocolNumber { + if len(ipHdr.Payload()) < header.ICMPv6MinimumSize || header.ICMPv6(ipHdr.Payload()).Type() != header.ICMPv6EchoRequest { + return nil, false + } + } + replySource := ipHdr.DestinationAddr() + if source.Is6() { + replySource = source + } + return buildRejectICMPv6(ipHdr, header.ICMPv6NetworkUnreachable, replySource, headroom) + default: + return nil, false + } +} diff --git a/flow_rewrite.go b/flow_rewrite.go new file mode 100644 index 00000000..e4d79673 --- /dev/null +++ b/flow_rewrite.go @@ -0,0 +1,350 @@ +package tun + +import ( + "encoding/binary" + + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" +) + +type rewriteRule struct { + sourceAddress tcpip.Address + sourcePort uint16 + rewriteSourcePort bool + destinationAddress tcpip.Address + destinationPort uint16 + rewriteDestinationPort bool +} + +func applyRewrite(packet *forwardPacket, rule *rewriteRule) { + oldSource := packet.network.SourceAddress() + oldDestination := packet.network.DestinationAddress() + newSource := oldSource + newDestination := oldDestination + if rule.sourceAddress.Len() > 0 { + newSource = rule.sourceAddress + } + if rule.destinationAddress.Len() > 0 { + newDestination = rule.destinationAddress + } + if ipHdr, isIPv4 := packet.network.(header.IPv4); isIPv4 { + if newSource != oldSource { + ipHdr.SetSourceAddressWithChecksumUpdate(newSource) + } + if newDestination != oldDestination { + ipHdr.SetDestinationAddressWithChecksumUpdate(newDestination) + } + } else { + if newSource != oldSource { + packet.network.SetSourceAddress(newSource) + } + if newDestination != oldDestination { + packet.network.SetDestinationAddress(newDestination) + } + } + transport := packet.transport + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + if newSource != oldSource { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSource, true) + } + if newDestination != oldDestination { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestination, true) + } + if rule.rewriteSourcePort { + tcpHdr.SetSourcePortWithChecksumUpdate(rule.sourcePort) + } + if rule.rewriteDestinationPort { + tcpHdr.SetDestinationPortWithChecksumUpdate(rule.destinationPort) + } + case uint8(header.UDPProtocolNumber): + if len(transport) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(transport) + if packet.ipVersion == 4 && udpHdr.Checksum() == 0 { + if rule.rewriteSourcePort { + udpHdr.SetSourcePort(rule.sourcePort) + } + if rule.rewriteDestinationPort { + udpHdr.SetDestinationPort(rule.destinationPort) + } + return + } + if newSource != oldSource { + udpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSource, true) + } + if newDestination != oldDestination { + udpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestination, true) + } + if rule.rewriteSourcePort { + udpHdr.SetSourcePortWithChecksumUpdate(rule.sourcePort) + } + if rule.rewriteDestinationPort { + udpHdr.SetDestinationPortWithChecksumUpdate(rule.destinationPort) + } + case uint8(header.ICMPv4ProtocolNumber): + if len(transport) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(transport) + if rule.rewriteSourcePort { + icmpHdr.SetIdentWithChecksumUpdate(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdentWithChecksumUpdate(rule.destinationPort) + } + case uint8(header.ICMPv6ProtocolNumber): + if len(transport) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(transport) + if newSource != oldSource { + icmpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSource) + } + if newDestination != oldDestination { + icmpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestination) + } + if rule.rewriteSourcePort { + icmpHdr.SetIdentWithChecksumUpdate(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdentWithChecksumUpdate(rule.destinationPort) + } + } +} + +func applyRewriteRaw(packet *forwardPacket, rule *rewriteRule) { + if rule.sourceAddress.Len() > 0 { + packet.network.SetSourceAddress(rule.sourceAddress) + } + if rule.destinationAddress.Len() > 0 { + packet.network.SetDestinationAddress(rule.destinationAddress) + } + transport := packet.transport + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + if rule.rewriteSourcePort { + tcpHdr.SetSourcePort(rule.sourcePort) + } + if rule.rewriteDestinationPort { + tcpHdr.SetDestinationPort(rule.destinationPort) + } + case uint8(header.UDPProtocolNumber): + if len(transport) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(transport) + if rule.rewriteSourcePort { + udpHdr.SetSourcePort(rule.sourcePort) + } + if rule.rewriteDestinationPort { + udpHdr.SetDestinationPort(rule.destinationPort) + } + case uint8(header.ICMPv4ProtocolNumber): + if len(transport) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(transport) + if rule.rewriteSourcePort { + icmpHdr.SetIdent(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdent(rule.destinationPort) + } + case uint8(header.ICMPv6ProtocolNumber): + if len(transport) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(transport) + if rule.rewriteSourcePort { + icmpHdr.SetIdent(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdent(rule.destinationPort) + } + } +} + +func recomputeChecksums(packet *forwardPacket) { + if ipHdr, isIPv4 := packet.network.(header.IPv4); isIPv4 { + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } + transport := packet.transport + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + tcpHdr.SetChecksum(0) + payloadChecksum := checksum.Checksum(tcpHdr.Payload(), 0) + pseudoChecksum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, packet.network.SourceAddressSlice(), packet.network.DestinationAddressSlice(), uint16(len(transport))) + tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum))) + case uint8(header.UDPProtocolNumber): + if len(transport) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(transport) + if packet.ipVersion == 4 && udpHdr.Checksum() == 0 { + return + } + udpHdr.SetChecksum(0) + payloadChecksum := checksum.Checksum(udpHdr.Payload(), 0) + pseudoChecksum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, packet.network.SourceAddressSlice(), packet.network.DestinationAddressSlice(), udpHdr.Length()) + udpChecksum := ^udpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum)) + if udpChecksum == 0 { + udpChecksum = 0xffff + } + udpHdr.SetChecksum(udpChecksum) + case uint8(header.ICMPv4ProtocolNumber): + if len(transport) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(transport) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + case uint8(header.ICMPv6ProtocolNumber): + if len(transport) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(transport) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: packet.network.SourceAddressSlice(), + Dst: packet.network.DestinationAddressSlice(), + })) + } +} + +func clampTCPMSS(packet *forwardPacket, effectiveMTU uint32) { + if effectiveMTU == 0 || packet.protocol != uint8(header.TCPProtocolNumber) { + return + } + transport := packet.transport + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + tcpHeaderLength := int(tcpHdr.DataOffset()) + if tcpHeaderLength < header.TCPMinimumSize || tcpHeaderLength > len(transport) { + return + } + var networkHeaderLength int + switch packet.ipVersion { + case 4: + networkHeaderLength = len(packet.network.(header.IPv4)) - len(transport) + default: + networkHeaderLength = len(packet.network.(header.IPv6)) - len(transport) + } + if effectiveMTU <= uint32(networkHeaderLength+header.TCPMinimumSize) { + return + } + maxMSS := min(effectiveMTU-uint32(networkHeaderLength+header.TCPMinimumSize), header.TCPMaximumMSS) + options := tcpHdr.Options() + for i := 0; i < len(options); { + switch options[i] { + case header.TCPOptionEOL: + return + case header.TCPOptionNOP: + i++ + continue + case header.TCPOptionMSS: + if i+header.TCPOptionMSSLength > len(options) || options[i+1] != header.TCPOptionMSSLength { + return + } + currentMSS := binary.BigEndian.Uint16(options[i+2:]) + if uint32(currentMSS) <= maxMSS { + return + } + binary.BigEndian.PutUint16(options[i+2:], uint16(maxMSS)) + return + default: + if i+2 > len(options) { + return + } + optionLength := int(options[i+1]) + if optionLength < 2 || i+optionLength > len(options) { + return + } + i += optionLength + } + } +} + +func rewriteEmbeddedDestination(embedded *embeddedPacket, destination tcpip.Address, selector uint16, remapSelector bool) { + oldDestination := embedded.network.DestinationAddress() + if ipHdr, isIPv4 := embedded.network.(header.IPv4); isIPv4 { + ipHdr.SetDestinationAddressWithChecksumUpdate(destination) + } else { + embedded.network.SetDestinationAddress(destination) + } + rewriteEmbeddedSelector(embedded, oldDestination, destination, selector, remapSelector, true) +} + +func rewriteEmbeddedSource(embedded *embeddedPacket, source tcpip.Address, selector uint16, remapSelector bool) { + oldSource := embedded.network.SourceAddress() + if ipHdr, isIPv4 := embedded.network.(header.IPv4); isIPv4 { + ipHdr.SetSourceAddressWithChecksumUpdate(source) + } else { + embedded.network.SetSourceAddress(source) + } + rewriteEmbeddedSelector(embedded, oldSource, source, selector, remapSelector, false) +} + +func rewriteEmbeddedSelector(embedded *embeddedPacket, oldAddress, newAddress tcpip.Address, selector uint16, remapSelector bool, destinationSide bool) { + if !remapSelector { + return + } + payload := embedded.payload + _, isIPv4 := embedded.network.(header.IPv4) + switch embedded.protocol { + case uint8(header.TCPProtocolNumber): + if len(payload) >= 4 { + if destinationSide { + binary.BigEndian.PutUint16(payload[2:], selector) + } else { + binary.BigEndian.PutUint16(payload[0:], selector) + } + } + case uint8(header.UDPProtocolNumber): + if len(payload) >= header.UDPMinimumSize { + udpHdr := header.UDP(payload) + if isIPv4 && udpHdr.Checksum() == 0 { + if destinationSide { + udpHdr.SetDestinationPort(selector) + } else { + udpHdr.SetSourcePort(selector) + } + } else { + if oldAddress != newAddress { + udpHdr.UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress, true) + } + if destinationSide { + udpHdr.SetDestinationPortWithChecksumUpdate(selector) + } else { + udpHdr.SetSourcePortWithChecksumUpdate(selector) + } + } + } + case uint8(header.ICMPv4ProtocolNumber): + if len(payload) >= header.ICMPv4MinimumSize { + header.ICMPv4(payload).SetIdentWithChecksumUpdate(selector) + } + case uint8(header.ICMPv6ProtocolNumber): + if len(payload) >= header.ICMPv6MinimumSize { + icmpHdr := header.ICMPv6(payload) + if oldAddress != newAddress { + icmpHdr.UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress) + } + icmpHdr.SetIdentWithChecksumUpdate(selector) + } + } +} diff --git a/internal/gtcpip/README.md b/gtcpip/README.md similarity index 100% rename from internal/gtcpip/README.md rename to gtcpip/README.md diff --git a/internal/gtcpip/checksum/checksum.go b/gtcpip/checksum/checksum.go similarity index 100% rename from internal/gtcpip/checksum/checksum.go rename to gtcpip/checksum/checksum.go diff --git a/internal/gtcpip/checksum/checksum_default.go b/gtcpip/checksum/checksum_default.go similarity index 100% rename from internal/gtcpip/checksum/checksum_default.go rename to gtcpip/checksum/checksum_default.go diff --git a/internal/gtcpip/checksum/checksum_ts.go b/gtcpip/checksum/checksum_ts.go similarity index 100% rename from internal/gtcpip/checksum/checksum_ts.go rename to gtcpip/checksum/checksum_ts.go diff --git a/internal/gtcpip/checksum/checksum_unsafe.go b/gtcpip/checksum/checksum_unsafe.go similarity index 100% rename from internal/gtcpip/checksum/checksum_unsafe.go rename to gtcpip/checksum/checksum_unsafe.go diff --git a/internal/gtcpip/errors.go b/gtcpip/errors.go similarity index 100% rename from internal/gtcpip/errors.go rename to gtcpip/errors.go diff --git a/internal/gtcpip/header/checksum.go b/gtcpip/header/checksum.go similarity index 97% rename from internal/gtcpip/header/checksum.go rename to gtcpip/header/checksum.go index 2c21e6d3..303502cc 100644 --- a/internal/gtcpip/header/checksum.go +++ b/gtcpip/header/checksum.go @@ -20,8 +20,8 @@ import ( "encoding/binary" "fmt" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) // PseudoHeaderChecksum calculates the pseudo-header checksum for the given diff --git a/internal/gtcpip/header/eth.go b/gtcpip/header/eth.go similarity index 99% rename from internal/gtcpip/header/eth.go rename to gtcpip/header/eth.go index 9d876ee6..613a72c6 100644 --- a/internal/gtcpip/header/eth.go +++ b/gtcpip/header/eth.go @@ -17,7 +17,7 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/icmpv4.go b/gtcpip/header/icmpv4.go similarity index 98% rename from internal/gtcpip/header/icmpv4.go rename to gtcpip/header/icmpv4.go index 580101c0..3b481041 100644 --- a/internal/gtcpip/header/icmpv4.go +++ b/gtcpip/header/icmpv4.go @@ -17,8 +17,8 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) // ICMPv4 represents an ICMPv4 header stored in a byte array. diff --git a/internal/gtcpip/header/icmpv6.go b/gtcpip/header/icmpv6.go similarity index 98% rename from internal/gtcpip/header/icmpv6.go rename to gtcpip/header/icmpv6.go index 520b4036..7eae97ab 100644 --- a/internal/gtcpip/header/icmpv6.go +++ b/gtcpip/header/icmpv6.go @@ -17,8 +17,8 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) // ICMPv6 represents an ICMPv6 header stored in a byte array. diff --git a/internal/gtcpip/header/interfaces.go b/gtcpip/header/interfaces.go similarity index 98% rename from internal/gtcpip/header/interfaces.go rename to gtcpip/header/interfaces.go index fc13100c..c0bb410c 100644 --- a/internal/gtcpip/header/interfaces.go +++ b/gtcpip/header/interfaces.go @@ -17,7 +17,7 @@ package header import ( "net/netip" - tcpip "github.com/sagernet/sing-tun/internal/gtcpip" + tcpip "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/ipv4.go b/gtcpip/header/ipv4.go similarity index 99% rename from internal/gtcpip/header/ipv4.go rename to gtcpip/header/ipv4.go index ad06f38c..d5ffbf1d 100644 --- a/internal/gtcpip/header/ipv4.go +++ b/gtcpip/header/ipv4.go @@ -20,8 +20,8 @@ import ( "net/netip" "time" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing/common" ) diff --git a/internal/gtcpip/header/ipv6.go b/gtcpip/header/ipv6.go similarity index 99% rename from internal/gtcpip/header/ipv6.go rename to gtcpip/header/ipv6.go index 1a5a7a05..4de30737 100644 --- a/internal/gtcpip/header/ipv6.go +++ b/gtcpip/header/ipv6.go @@ -20,7 +20,7 @@ import ( "fmt" "net/netip" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/ipv6_extension_headers.go b/gtcpip/header/ipv6_extension_headers.go similarity index 99% rename from internal/gtcpip/header/ipv6_extension_headers.go rename to gtcpip/header/ipv6_extension_headers.go index 20064d8b..6c48b1bf 100644 --- a/internal/gtcpip/header/ipv6_extension_headers.go +++ b/gtcpip/header/ipv6_extension_headers.go @@ -20,7 +20,7 @@ import ( "fmt" "math" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing/common" ) diff --git a/internal/gtcpip/header/ipv6_fragment.go b/gtcpip/header/ipv6_fragment.go similarity index 99% rename from internal/gtcpip/header/ipv6_fragment.go rename to gtcpip/header/ipv6_fragment.go index 49aaca71..38f0b202 100644 --- a/internal/gtcpip/header/ipv6_fragment.go +++ b/gtcpip/header/ipv6_fragment.go @@ -17,7 +17,7 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/ndp_neighbor_advert.go b/gtcpip/header/ndp_neighbor_advert.go similarity index 98% rename from internal/gtcpip/header/ndp_neighbor_advert.go rename to gtcpip/header/ndp_neighbor_advert.go index 7a934cce..8f36765a 100644 --- a/internal/gtcpip/header/ndp_neighbor_advert.go +++ b/gtcpip/header/ndp_neighbor_advert.go @@ -14,7 +14,7 @@ package header -import "github.com/sagernet/sing-tun/internal/gtcpip" +import "github.com/sagernet/sing-tun/gtcpip" // NDPNeighborAdvert is an NDP Neighbor Advertisement message. It will // only contain the body of an ICMPv6 packet. diff --git a/internal/gtcpip/header/ndp_neighbor_solicit.go b/gtcpip/header/ndp_neighbor_solicit.go similarity index 97% rename from internal/gtcpip/header/ndp_neighbor_solicit.go rename to gtcpip/header/ndp_neighbor_solicit.go index 61d61a8a..b4af20ce 100644 --- a/internal/gtcpip/header/ndp_neighbor_solicit.go +++ b/gtcpip/header/ndp_neighbor_solicit.go @@ -14,7 +14,7 @@ package header -import "github.com/sagernet/sing-tun/internal/gtcpip" +import "github.com/sagernet/sing-tun/gtcpip" // NDPNeighborSolicit is an NDP Neighbor Solicitation message. It will only // contain the body of an ICMPv6 packet. diff --git a/internal/gtcpip/header/ndp_options.go b/gtcpip/header/ndp_options.go similarity index 99% rename from internal/gtcpip/header/ndp_options.go rename to gtcpip/header/ndp_options.go index ba293398..c545120d 100644 --- a/internal/gtcpip/header/ndp_options.go +++ b/gtcpip/header/ndp_options.go @@ -23,7 +23,7 @@ import ( "math" "time" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing/common" ) @@ -878,7 +878,7 @@ func (o NDPDNSSearchList) iterDomainNames(fn func(string)) error { } // Copy the label and add a trailing period. - for i := 0; i < labelLen; i++ { + for i := range labelLen { b, err := searchList.ReadByte() if err != nil { if err != io.EOF { diff --git a/internal/gtcpip/header/ndp_router_advert.go b/gtcpip/header/ndp_router_advert.go similarity index 100% rename from internal/gtcpip/header/ndp_router_advert.go rename to gtcpip/header/ndp_router_advert.go diff --git a/internal/gtcpip/header/ndp_router_solicit.go b/gtcpip/header/ndp_router_solicit.go similarity index 100% rename from internal/gtcpip/header/ndp_router_solicit.go rename to gtcpip/header/ndp_router_solicit.go diff --git a/internal/gtcpip/header/ndpoptionidentifier_string.go b/gtcpip/header/ndpoptionidentifier_string.go similarity index 100% rename from internal/gtcpip/header/ndpoptionidentifier_string.go rename to gtcpip/header/ndpoptionidentifier_string.go diff --git a/internal/gtcpip/header/netip.go b/gtcpip/header/netip.go similarity index 100% rename from internal/gtcpip/header/netip.go rename to gtcpip/header/netip.go diff --git a/internal/gtcpip/header/tcp.go b/gtcpip/header/tcp.go similarity index 98% rename from internal/gtcpip/header/tcp.go rename to gtcpip/header/tcp.go index 1b58df86..7ab428f8 100644 --- a/internal/gtcpip/header/tcp.go +++ b/gtcpip/header/tcp.go @@ -17,9 +17,9 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/seqnum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/seqnum" "github.com/google/btree" ) @@ -476,20 +476,14 @@ func ParseSynOptions(opts []byte, isAck bool) TCPSynOptions { if mss == 0 { return synOpts } - synOpts.MSS = mss - if mss < TCPMinimumSendMSS { - synOpts.MSS = TCPMinimumSendMSS - } + synOpts.MSS = max(mss, TCPMinimumSendMSS) i += 4 case TCPOptionWS: if i+3 > limit || opts[i+1] != 3 { return synOpts } - ws := int(opts[i+2]) - if ws > MaxWndScale { - ws = MaxWndScale - } + ws := min(int(opts[i+2]), MaxWndScale) synOpts.WS = ws i += 3 @@ -561,7 +555,7 @@ func ParseTCPOptions(b []byte) TCPOptions { } numBlocks := (sackOptionLen - 2) / 8 opts.SACKBlocks = []SACKBlock{} - for j := 0; j < numBlocks; j++ { + for j := range numBlocks { start := binary.BigEndian.Uint32(b[i+2+j*8:]) end := binary.BigEndian.Uint32(b[i+2+j*8+4:]) opts.SACKBlocks = append(opts.SACKBlocks, SACKBlock{ @@ -646,10 +640,7 @@ func EncodeSACKBlocks(sackBlocks []SACKBlock, b []byte) int { if len(sackBlocks) == 0 { return 0 } - l := len(sackBlocks) - if l > TCPMaxSACKBlocks { - l = TCPMaxSACKBlocks - } + l := min(len(sackBlocks), TCPMaxSACKBlocks) if ll := (len(b) - 2) / 8; ll < l { l = ll } diff --git a/internal/gtcpip/header/udp.go b/gtcpip/header/udp.go similarity index 98% rename from internal/gtcpip/header/udp.go rename to gtcpip/header/udp.go index a995a172..ce7708e1 100644 --- a/internal/gtcpip/header/udp.go +++ b/gtcpip/header/udp.go @@ -18,8 +18,8 @@ import ( "encoding/binary" "math" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) const ( diff --git a/internal/gtcpip/seqnum/seqnum.go b/gtcpip/seqnum/seqnum.go similarity index 100% rename from internal/gtcpip/seqnum/seqnum.go rename to gtcpip/seqnum/seqnum.go diff --git a/internal/gtcpip/tcpip.go b/gtcpip/tcpip.go similarity index 93% rename from internal/gtcpip/tcpip.go rename to gtcpip/tcpip.go index 60d2892e..a3dbca84 100644 --- a/internal/gtcpip/tcpip.go +++ b/gtcpip/tcpip.go @@ -245,6 +245,53 @@ func (a Address) Len() int { return a.length } +// String implements the fmt.Stringer interface. +func (a Address) String() string { + switch l := a.Len(); l { + case 4: + return fmt.Sprintf("%d.%d.%d.%d", int(a.addr[0]), int(a.addr[1]), int(a.addr[2]), int(a.addr[3])) + case 16: + // Find the longest subsequence of hexadecimal zeros. + start, end := -1, -1 + for i := 0; i < a.Len(); i += 2 { + j := i + for j < a.Len() && a.addr[j] == 0 && a.addr[j+1] == 0 { + j += 2 + } + if j > i+2 && j-i > end-start { + start, end = i, j + } + } + + var b strings.Builder + for i := 0; i < a.Len(); i += 2 { + if i == start { + b.WriteString("::") + i = end + if end >= a.Len() { + break + } + } else if i > 0 { + b.WriteByte(':') + } + v := uint16(a.addr[i+0])<<8 | uint16(a.addr[i+1]) + if v == 0 { + b.WriteByte('0') + } else { + const digits = "0123456789abcdef" + for i := uint(3); i < 4; i-- { + if v := v >> (i * 4); v != 0 { + b.WriteByte(digits[v&0xf]) + } + } + } + } + return b.String() + default: + return fmt.Sprintf("%x", a.addr[:l]) + } +} + // WithPrefix returns the address with a prefix that represents a point subnet. func (a Address) WithPrefix() AddressWithPrefix { return AddressWithPrefix{ @@ -541,7 +588,7 @@ func (a AddressWithPrefix) Subnet() Subnet { address: a.Address, mask: AddressMask{length: addrLen}, } - for i := 0; i < addrLen; i++ { + for i := range addrLen { sub.mask.mask[i] = 0xff } return sub @@ -550,7 +597,7 @@ func (a AddressWithPrefix) Subnet() Subnet { sa := Address{length: addrLen} sm := AddressMask{length: addrLen} n := uint(a.PrefixLen) - for i := 0; i < addrLen; i++ { + for i := range addrLen { if n >= 8 { sa.addr[i] = a.Address.addr[i] sm.mask[i] = 0xff diff --git a/internal/checksum_test/sum_bench_test.go b/internal/checksum_test/sum_bench_test.go index 35ee021c..fb4aaa4c 100644 --- a/internal/checksum_test/sum_bench_test.go +++ b/internal/checksum_test/sum_bench_test.go @@ -4,13 +4,13 @@ import ( "crypto/rand" "testing" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/tschecksum" ) func BenchmarkTsChecksum(b *testing.B) { packet := make([][]byte, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { packet[i] = make([]byte, 1500) rand.Read(packet[i]) } @@ -22,7 +22,7 @@ func BenchmarkTsChecksum(b *testing.B) { func BenchmarkGChecksum(b *testing.B) { packet := make([][]byte, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { packet[i] = make([]byte, 1500) rand.Read(packet[i]) } diff --git a/internal/fdbased_darwin/endpoint.go b/internal/fdbased_darwin/endpoint.go index f26bfe30..05371e77 100644 --- a/internal/fdbased_darwin/endpoint.go +++ b/internal/fdbased_darwin/endpoint.go @@ -35,6 +35,9 @@ // only use the first FD to write outbound packets. Once 5 tuple hashes for // all outbound packets are available we will make use of all underlying FD's to // write outbound packets. + +//go:build darwin + package fdbased import ( @@ -46,7 +49,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/sing-tun/internal/rawfile_darwin" + rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing/common" "golang.org/x/sys/unix" diff --git a/internal/fdbased_darwin/endpoint_mutex.go b/internal/fdbased_darwin/endpoint_mutex.go index d05b2640..4f2d4b83 100644 --- a/internal/fdbased_darwin/endpoint_mutex.go +++ b/internal/fdbased_darwin/endpoint_mutex.go @@ -1,3 +1,5 @@ +//go:build darwin + package fdbased import ( @@ -92,5 +94,5 @@ func endpointinitLockNames() {} func init() { endpointinitLockNames() - endpointprefixIndex = locking.NewMutexClass(reflect.TypeOf(endpointRWMutex{}), endpointlockNames) + endpointprefixIndex = locking.NewMutexClass(reflect.TypeFor[endpointRWMutex](), endpointlockNames) } diff --git a/internal/fdbased_darwin/errno.go b/internal/fdbased_darwin/errno.go index 074f4e2e..8c6f9663 100644 --- a/internal/fdbased_darwin/errno.go +++ b/internal/fdbased_darwin/errno.go @@ -1,3 +1,5 @@ +//go:build darwin + package fdbased import ( diff --git a/internal/fdbased_darwin/packet_dispatchers.go b/internal/fdbased_darwin/packet_dispatchers.go index a006d411..afe362f6 100644 --- a/internal/fdbased_darwin/packet_dispatchers.go +++ b/internal/fdbased_darwin/packet_dispatchers.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build darwin + package fdbased import ( @@ -19,7 +21,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/stack/gro" - "github.com/sagernet/sing-tun/internal/rawfile_darwin" + rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing-tun/internal/stopfd_darwin" "golang.org/x/sys/unix" @@ -88,6 +90,8 @@ type recvMMsgDispatcher struct { // fd is the file descriptor used to send and receive packets. fd int + poller *rawfile.Poller + // e is the endpoint this dispatcher is attached to. e *endpoint @@ -121,9 +125,15 @@ func newRecvMMsgDispatcher(fd int, e *endpoint, opts *Options) (linkDispatcher, } else { batchSize = 1 } + poller, err := rawfile.NewPoller(stopFD.ReadFD, fd) + if err != nil { + stopFD.Close() + return nil, err + } d := &recvMMsgDispatcher{ StopFD: stopFD, fd: fd, + poller: poller, e: e, bufs: make([]*iovecBuffer, batchSize), msgHdrs: make([]rawfile.MsgHdrX, batchSize), @@ -142,6 +152,7 @@ func (d *recvMMsgDispatcher) release() { for _, iov := range d.bufs { iov.release() } + _ = d.poller.Close() d.mgr.close() } @@ -159,7 +170,7 @@ func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { d.msgHdrs[k].Msg.SetIovlen(iovLen) } - nMsgs, errno := rawfile.BlockingRecvMMsgUntilStopped(d.ReadFD, d.fd, d.msgHdrs) + nMsgs, errno := rawfile.BlockingRecvMMsgUntilStopped(d.poller, d.fd, d.msgHdrs) if errno != 0 { return false, TranslateErrno(errno) } @@ -177,7 +188,7 @@ func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { d.gro.Dispatcher = dsp defer d.pkts.Reset() - for k := 0; k < nMsgs; k++ { + for k := range nMsgs { n := int(d.msgHdrs[k].DataLen) payload := d.bufs[k].pullBuffer(n) pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ diff --git a/internal/fdbased_darwin/processor_mutex.go b/internal/fdbased_darwin/processor_mutex.go index cd297d2a..87e1801c 100644 --- a/internal/fdbased_darwin/processor_mutex.go +++ b/internal/fdbased_darwin/processor_mutex.go @@ -1,3 +1,5 @@ +//go:build darwin + package fdbased import ( @@ -60,5 +62,5 @@ func processorinitLockNames() {} func init() { processorinitLockNames() - processorprefixIndex = locking.NewMutexClass(reflect.TypeOf(processorMutex{}), processorlockNames) + processorprefixIndex = locking.NewMutexClass(reflect.TypeFor[processorMutex](), processorlockNames) } diff --git a/internal/fdbased_darwin/processors.go b/internal/fdbased_darwin/processors.go index 9df6cfa4..1ceca834 100644 --- a/internal/fdbased_darwin/processors.go +++ b/internal/fdbased_darwin/processors.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build darwin + package fdbased import ( diff --git a/internal/rawfile_darwin/rawfile.go b/internal/rawfile_darwin/rawfile.go index b73bd82f..e586ef2e 100644 --- a/internal/rawfile_darwin/rawfile.go +++ b/internal/rawfile_darwin/rawfile.go @@ -1,7 +1,8 @@ +//go:build darwin + package rawfile import ( - "reflect" "unsafe" "golang.org/x/sys/unix" @@ -25,12 +26,8 @@ func IovecFromBytes(bs []byte) unix.Iovec { return iov } -func bytesFromIovec(iov unix.Iovec) (bs []byte) { - sh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) - sh.Data = uintptr(unsafe.Pointer(iov.Base)) - sh.Len = int(iov.Len) - sh.Cap = int(iov.Len) - return +func bytesFromIovec(iov unix.Iovec) []byte { + return unsafe.Slice(iov.Base, iov.Len) } // AppendIovecFromBytes returns append(iovs, IovecFromBytes(bs)). If len(bs) == @@ -56,6 +53,7 @@ type MsgHdrX struct { } func NonBlockingSendMMsg(fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { + //nolint:staticcheck n, _, e := unix.RawSyscall6(unix.SYS_SENDMSG_X, uintptr(fd), uintptr(unsafe.Pointer(&msgHdrs[0])), uintptr(len(msgHdrs)), unix.MSG_DONTWAIT, 0, 0) return int(n), e } @@ -66,12 +64,14 @@ const SizeofMsgHdrX = unsafe.Sizeof(MsgHdrX{}) // It fails if partial data is written. func NonBlockingWriteIovec(fd int, iovec []unix.Iovec) unix.Errno { iovecLen := uintptr(len(iovec)) + //nolint:staticcheck _, _, e := unix.RawSyscall(unix.SYS_WRITEV, uintptr(fd), uintptr(unsafe.Pointer(&iovec[0])), iovecLen) return e } -func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix.Errno) { +func BlockingReadvUntilStopped(poller *Poller, fd int, iovecs []unix.Iovec) (int, unix.Errno) { for { + //nolint:staticcheck n, _, e := unix.RawSyscall(unix.SYS_READV, uintptr(fd), uintptr(unsafe.Pointer(&iovecs[0])), uintptr(len(iovecs))) if e == 0 { return int(n), 0 @@ -79,7 +79,7 @@ func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix. if e != 0 && e != unix.EWOULDBLOCK { return 0, e } - stopped, e := BlockingPollUntilStopped(efd, fd, unix.POLLIN) + stopped, e := poller.Wait() if stopped { return -1, e } @@ -89,8 +89,9 @@ func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix. } } -func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { +func BlockingRecvMMsgUntilStopped(poller *Poller, fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { for { + //nolint:staticcheck n, _, e := unix.RawSyscall6(unix.SYS_RECVMSG_X, uintptr(fd), uintptr(unsafe.Pointer(&msgHdrs[0])), uintptr(len(msgHdrs)), unix.MSG_DONTWAIT, 0, 0) if e == 0 { return int(n), e @@ -100,7 +101,7 @@ func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix return 0, e } - stopped, e := BlockingPollUntilStopped(efd, fd, unix.POLLIN) + stopped, e := poller.Wait() if stopped { return -1, e } @@ -110,71 +111,58 @@ func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix } } -func BlockingPollUntilStopped(efd int, fd int, events int16) (bool, unix.Errno) { - // Create kqueue +type Poller struct { + kq int + efd int + fd int +} + +func NewPoller(efd int, fd int) (*Poller, error) { kq, err := unix.Kqueue() if err != nil { - return false, unix.Errno(err.(unix.Errno)) + return nil, err } - defer unix.Close(kq) - - // Prepare kevents for registration - var kevents []unix.Kevent_t - - // Always monitor efd for read events - kevents = append(kevents, unix.Kevent_t{ - Ident: uint64(efd), - Filter: unix.EVFILT_READ, - Flags: unix.EV_ADD | unix.EV_ENABLE, - }) - - // Monitor fd based on requested events - // Convert poll events to kqueue filters - if events&unix.POLLIN != 0 { - kevents = append(kevents, unix.Kevent_t{ - Ident: uint64(fd), + kevents := []unix.Kevent_t{ + { + Ident: uint64(efd), Filter: unix.EVFILT_READ, Flags: unix.EV_ADD | unix.EV_ENABLE, - }) - } - if events&unix.POLLOUT != 0 { - kevents = append(kevents, unix.Kevent_t{ + }, + { Ident: uint64(fd), - Filter: unix.EVFILT_WRITE, + Filter: unix.EVFILT_READ, Flags: unix.EV_ADD | unix.EV_ENABLE, - }) + }, } - - // Register events _, err = unix.Kevent(kq, kevents, nil, nil) if err != nil { - return false, unix.Errno(err.(unix.Errno)) + unix.Close(kq) + return nil, err } + return &Poller{kq: kq, efd: efd, fd: fd}, nil +} - // Wait for events (blocking) - revents := make([]unix.Kevent_t, len(kevents)) - n, err := unix.Kevent(kq, nil, revents, nil) +func (p *Poller) Wait() (bool, unix.Errno) { + var revents [2]unix.Kevent_t + n, err := unix.Kevent(p.kq, nil, revents[:], nil) if err != nil { - return false, unix.Errno(err.(unix.Errno)) + return false, err.(unix.Errno) } - // Check results var efdHasData bool var errno unix.Errno - for i := 0; i < n; i++ { + for i := range n { ev := &revents[i] - if int(ev.Ident) == efd && ev.Filter == unix.EVFILT_READ { + if int(ev.Ident) == p.efd && ev.Filter == unix.EVFILT_READ { efdHasData = true } - if int(ev.Ident) == fd { - // Check for errors or EOF + if int(ev.Ident) == p.fd { if ev.Flags&unix.EV_EOF != 0 { errno = unix.ECONNRESET } else if ev.Flags&unix.EV_ERROR != 0 { - // Extract error from Data field if ev.Data != 0 { errno = unix.Errno(ev.Data) } else { @@ -186,3 +174,7 @@ func BlockingPollUntilStopped(efd int, fd int, events int16) (bool, unix.Errno) return efdHasData, errno } + +func (p *Poller) Close() error { + return unix.Close(p.kq) +} diff --git a/internal/stopfd_darwin/stopfd.go b/internal/stopfd_darwin/stopfd.go index fdc39739..0a8f9802 100644 --- a/internal/stopfd_darwin/stopfd.go +++ b/internal/stopfd_darwin/stopfd.go @@ -1,3 +1,5 @@ +//go:build darwin + package stopfd import ( diff --git a/internal/winfw/winfw.go b/internal/winfw/winfw.go index f8f17bb4..8d5b80fa 100644 --- a/internal/winfw/winfw.go +++ b/internal/winfw/winfw.go @@ -98,105 +98,105 @@ func firewallRuleAdd(name, description, group, appPath, serviceName, ports, remo if profile == NET_FW_PROFILE2_CURRENT { currentProfiles, err := oleutil.GetProperty(fwPolicy, "CurrentProfileTypes") if err != nil { - return false, fmt.Errorf("Failed to get CurrentProfiles: %s", err) + return false, fmt.Errorf("failed to get CurrentProfiles: %s", err) } profile = currentProfiles.Value().(int32) } unknownRules, err := oleutil.GetProperty(fwPolicy, "Rules") if err != nil { - return false, fmt.Errorf("Failed to get Rules: %s", err) + return false, fmt.Errorf("failed to get Rules: %s", err) } rules := unknownRules.ToIDispatch() if ok, err := FirewallRuleExistsByName(rules, name); err != nil { - return false, fmt.Errorf("Error while checking rules for duplicate: %s", err) + return false, fmt.Errorf("error while checking rules for duplicate: %s", err) } else if ok { return false, nil } unknown2, err := oleutil.CreateObject("HNetCfg.FWRule") if err != nil { - return false, fmt.Errorf("Error creating Rule object: %s", err) + return false, fmt.Errorf("error creating Rule object: %s", err) } defer unknown2.Release() fwRule, err := unknown2.QueryInterface(ole.IID_IDispatch) if err != nil { - return false, fmt.Errorf("Error creating Rule object (2): %s", err) + return false, fmt.Errorf("error creating Rule object (2): %s", err) } defer fwRule.Release() if _, err := oleutil.PutProperty(fwRule, "Name", name); err != nil { - return false, fmt.Errorf("Error setting property (Name) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Name) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Description", description); err != nil { - return false, fmt.Errorf("Error setting property (Description) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Description) of Rule: %s", err) } if appPath != "" { if _, err := oleutil.PutProperty(fwRule, "Applicationname", appPath); err != nil { - return false, fmt.Errorf("Error setting property (Applicationname) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Applicationname) of Rule: %s", err) } } if serviceName != "" { if _, err := oleutil.PutProperty(fwRule, "ServiceName", serviceName); err != nil { - return false, fmt.Errorf("Error setting property (ServiceName) of Rule: %s", err) + return false, fmt.Errorf("error setting property (ServiceName) of Rule: %s", err) } } if protocol != 0 { if _, err := oleutil.PutProperty(fwRule, "Protocol", protocol); err != nil { - return false, fmt.Errorf("Error setting property (Protocol) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Protocol) of Rule: %s", err) } } if icmpTypes != "" { if _, err := oleutil.PutProperty(fwRule, "IcmpTypesAndCodes", icmpTypes); err != nil { - return false, fmt.Errorf("Error setting property (IcmpTypesAndCodes) of Rule: %s", err) + return false, fmt.Errorf("error setting property (IcmpTypesAndCodes) of Rule: %s", err) } } if ports != "" { if _, err := oleutil.PutProperty(fwRule, "LocalPorts", ports); err != nil { - return false, fmt.Errorf("Error setting property (LocalPorts) of Rule: %s", err) + return false, fmt.Errorf("error setting property (LocalPorts) of Rule: %s", err) } } if remotePorts != "" { if _, err := oleutil.PutProperty(fwRule, "RemotePorts", remotePorts); err != nil { - return false, fmt.Errorf("Error setting property (RemotePorts) of Rule: %s", err) + return false, fmt.Errorf("error setting property (RemotePorts) of Rule: %s", err) } } if localAddresses != "" { if _, err := oleutil.PutProperty(fwRule, "LocalAddresses", localAddresses); err != nil { - return false, fmt.Errorf("Error setting property (LocalAddresses) of Rule: %s", err) + return false, fmt.Errorf("error setting property (LocalAddresses) of Rule: %s", err) } } if remoteAddresses != "" { if _, err := oleutil.PutProperty(fwRule, "RemoteAddresses", remoteAddresses); err != nil { - return false, fmt.Errorf("Error setting property (RemoteAddresses) of Rule: %s", err) + return false, fmt.Errorf("error setting property (RemoteAddresses) of Rule: %s", err) } } if direction != 0 { if _, err := oleutil.PutProperty(fwRule, "Direction", direction); err != nil { - return false, fmt.Errorf("Error setting property (Direction) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Direction) of Rule: %s", err) } } if _, err := oleutil.PutProperty(fwRule, "Enabled", enabled); err != nil { - return false, fmt.Errorf("Error setting property (Enabled) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Enabled) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Grouping", group); err != nil { - return false, fmt.Errorf("Error setting property (Grouping) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Grouping) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Profiles", profile); err != nil { - return false, fmt.Errorf("Error setting property (Profiles) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Profiles) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Action", action); err != nil { - return false, fmt.Errorf("Error setting property (Action) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Action) of Rule: %s", err) } if edgeTraversal { if _, err := oleutil.PutProperty(fwRule, "EdgeTraversal", edgeTraversal); err != nil { - return false, fmt.Errorf("Error setting property (EdgeTraversal) of Rule: %s", err) + return false, fmt.Errorf("error setting property (EdgeTraversal) of Rule: %s", err) } } if _, err := oleutil.CallMethod(rules, "Add", fwRule); err != nil { - return false, fmt.Errorf("Error adding Rule: %s", err) + return false, fmt.Errorf("error adding Rule: %s", err) } return true, nil @@ -205,13 +205,13 @@ func firewallRuleAdd(name, description, group, appPath, serviceName, ports, remo func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { enumProperty, err := rules.GetProperty("_NewEnum") if err != nil { - return false, fmt.Errorf("Failed to get enumeration property on Rules: %s", err) + return false, fmt.Errorf("failed to get enumeration property on Rules: %s", err) } defer enumProperty.Clear() enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) if err != nil { - return false, fmt.Errorf("Failed to cast enum to correct type: %s", err) + return false, fmt.Errorf("failed to cast enum to correct type: %s", err) } if enum == nil { return false, fmt.Errorf("can't get IEnumVARIANT, enum is nil") @@ -219,7 +219,7 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) { if err != nil { - return false, fmt.Errorf("Failed to seek next Rule item: %s", err) + return false, fmt.Errorf("failed to seek next Rule item: %s", err) } t, err := func() (bool, error) { @@ -227,7 +227,7 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { defer item.Release() if item, err := oleutil.GetProperty(item, "Name"); err != nil { - return false, fmt.Errorf("Failed to get Property (Name) of Rule") + return false, fmt.Errorf("failed to get Property (Name) of Rule") } else if item.ToString() == name { return true, nil } @@ -251,18 +251,18 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { func firewallAPIInit() (*ole.IUnknown, *ole.IDispatch, error) { err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) if err != nil { - return nil, nil, fmt.Errorf("Failed to initialize COM: %s", err) + return nil, nil, fmt.Errorf("failed to initialize COM: %s", err) } unknown, err := oleutil.CreateObject("HNetCfg.FwPolicy2") if err != nil { - return nil, nil, fmt.Errorf("Failed to create FwPolicy Object: %s", err) + return nil, nil, fmt.Errorf("failed to create FwPolicy Object: %s", err) } fwPolicy, err := unknown.QueryInterface(ole.IID_IDispatch) if err != nil { unknown.Release() - return nil, nil, fmt.Errorf("Failed to create FwPolicy Object (2): %s", err) + return nil, nil, fmt.Errorf("failed to create FwPolicy Object (2): %s", err) } return unknown, fwPolicy, nil diff --git a/internal/winipcfg/interface_change_handler.go b/internal/winipcfg/interface_change_handler.go index af29801a..b1669404 100644 --- a/internal/winipcfg/interface_change_handler.go +++ b/internal/winipcfg/interface_change_handler.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/luid.go b/internal/winipcfg/luid.go index 1f97314c..b7159cbc 100644 --- a/internal/winipcfg/luid.go +++ b/internal/winipcfg/luid.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/mksyscall.go b/internal/winipcfg/mksyscall.go index d62d38df..f07abb34 100644 --- a/internal/winipcfg/mksyscall.go +++ b/internal/winipcfg/mksyscall.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/netsh.go b/internal/winipcfg/netsh.go index 2c298cb7..51759103 100644 --- a/internal/winipcfg/netsh.go +++ b/internal/winipcfg/netsh.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. @@ -60,9 +62,10 @@ const ( func (luid LUID) fallbackSetDNSForFamily(family AddressFamily, dnses []netip.Addr) error { var templateFlush string - if family == windows.AF_INET { + switch family { + case windows.AF_INET: templateFlush = netshCmdTemplateFlush4 - } else if family == windows.AF_INET6 { + case windows.AF_INET6: templateFlush = netshCmdTemplateFlush6 } @@ -72,7 +75,7 @@ func (luid LUID) fallbackSetDNSForFamily(family AddressFamily, dnses []netip.Add return err } cmds = append(cmds, fmt.Sprintf(templateFlush, ipif.InterfaceIndex)) - for i := 0; i < len(dnses); i++ { + for i := range dnses { if dnses[i].Is4() && family == windows.AF_INET { cmds = append(cmds, fmt.Sprintf(netshCmdTemplateAdd4, ipif.InterfaceIndex, dnses[i].String())) } else if dnses[i].Is6() && family == windows.AF_INET6 { @@ -85,23 +88,23 @@ func (luid LUID) fallbackSetDNSForFamily(family AddressFamily, dnses []netip.Add func (luid LUID) fallbackSetDNSDomain(domain string) error { guid, err := luid.GUID() if err != nil { - return fmt.Errorf("Error converting luid to guid: %w", err) + return fmt.Errorf("error converting luid to guid: %w", err) } key, err := registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Adapters\\%v", guid), registry.QUERY_VALUE) if err != nil { - return fmt.Errorf("Error opening adapter-specific TCP/IP network registry key: %w", err) + return fmt.Errorf("error opening adapter-specific TCP/IP network registry key: %w", err) } paths, _, err := key.GetStringsValue("IpConfig") key.Close() if err != nil { - return fmt.Errorf("Error reading IpConfig registry key: %w", err) + return fmt.Errorf("error reading IpConfig registry key: %w", err) } if len(paths) == 0 { - return errors.New("No TCP/IP interfaces found on adapter") + return errors.New("no TCP/IP interfaces found on adapter") } key, err = registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("SYSTEM\\CurrentControlSet\\Services\\%s", paths[0]), registry.SET_VALUE) if err != nil { - return fmt.Errorf("Unable to open TCP/IP network registry key: %w", err) + return fmt.Errorf("unable to open TCP/IP network registry key: %w", err) } err = key.SetStringValue("Domain", domain) key.Close() diff --git a/internal/winipcfg/route_change_handler.go b/internal/winipcfg/route_change_handler.go index 4b78331e..63e7aa13 100644 --- a/internal/winipcfg/route_change_handler.go +++ b/internal/winipcfg/route_change_handler.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/types.go b/internal/winipcfg/types.go index 8e8f4a59..01b5cc05 100644 --- a/internal/winipcfg/types.go +++ b/internal/winipcfg/types.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. @@ -62,206 +64,206 @@ type IfType uint32 const ( IfTypeOther IfType = 1 // None of the below - IfTypeRegular1822 = 2 - IfTypeHdh1822 = 3 - IfTypeDdnX25 = 4 - IfTypeRfc877X25 = 5 - IfTypeEthernetCSMACD = 6 - IfTypeISO88023CSMACD = 7 - IfTypeISO88024Tokenbus = 8 - IfTypeISO88025Tokenring = 9 - IfTypeISO88026Man = 10 - IfTypeStarlan = 11 - IfTypeProteon10Mbit = 12 - IfTypeProteon80Mbit = 13 - IfTypeHyperchannel = 14 - IfTypeFddi = 15 - IfTypeLapB = 16 - IfTypeSdlc = 17 - IfTypeDs1 = 18 // DS1-MIB - IfTypeE1 = 19 // Obsolete; see DS1-MIB - IfTypeBasicISDN = 20 - IfTypePrimaryISDN = 21 - IfTypePropPoint2PointSerial = 22 // proprietary serial - IfTypePPP = 23 - IfTypeSoftwareLoopback = 24 - IfTypeEon = 25 // CLNP over IP - IfTypeEthernet3Mbit = 26 - IfTypeNsip = 27 // XNS over IP - IfTypeSlip = 28 // Generic Slip - IfTypeUltra = 29 // ULTRA Technologies - IfTypeDs3 = 30 // DS3-MIB - IfTypeSip = 31 // SMDS, coffee - IfTypeFramerelay = 32 // DTE only - IfTypeRs232 = 33 - IfTypePara = 34 // Parallel port - IfTypeArcnet = 35 - IfTypeArcnetPlus = 36 - IfTypeAtm = 37 // ATM cells - IfTypeMioX25 = 38 - IfTypeSonet = 39 // SONET or SDH - IfTypeX25Ple = 40 - IfTypeIso88022LLC = 41 - IfTypeLocaltalk = 42 - IfTypeSmdsDxi = 43 - IfTypeFramerelayService = 44 // FRNETSERV-MIB - IfTypeV35 = 45 - IfTypeHssi = 46 - IfTypeHippi = 47 - IfTypeModem = 48 // Generic Modem - IfTypeAal5 = 49 // AAL5 over ATM - IfTypeSonetPath = 50 - IfTypeSonetVt = 51 - IfTypeSmdsIcip = 52 // SMDS InterCarrier Interface - IfTypePropVirtual = 53 // Proprietary virtual/internal - IfTypePropMultiplexor = 54 // Proprietary multiplexing - IfTypeIEEE80212 = 55 // 100BaseVG - IfTypeFibrechannel = 56 - IfTypeHippiinterface = 57 - IfTypeFramerelayInterconnect = 58 // Obsolete, use 32 or 44 - IfTypeAflane8023 = 59 // ATM Emulated LAN for 802.3 - IfTypeAflane8025 = 60 // ATM Emulated LAN for 802.5 - IfTypeCctemul = 61 // ATM Emulated circuit - IfTypeFastether = 62 // Fast Ethernet (100BaseT) - IfTypeISDN = 63 // ISDN and X.25 - IfTypeV11 = 64 // CCITT V.11/X.21 - IfTypeV36 = 65 // CCITT V.36 - IfTypeG703_64k = 66 // CCITT G703 at 64Kbps - IfTypeG703_2mb = 67 // Obsolete; see DS1-MIB - IfTypeQllc = 68 // SNA QLLC - IfTypeFastetherFX = 69 // Fast Ethernet (100BaseFX) - IfTypeChannel = 70 - IfTypeIEEE80211 = 71 // Radio spread spectrum - IfTypeIBM370parchan = 72 // IBM System 360/370 OEMI Channel - IfTypeEscon = 73 // IBM Enterprise Systems Connection - IfTypeDlsw = 74 // Data Link Switching - IfTypeISDNS = 75 // ISDN S/T interface - IfTypeISDNU = 76 // ISDN U interface - IfTypeLapD = 77 // Link Access Protocol D - IfTypeIpswitch = 78 // IP Switching Objects - IfTypeRsrb = 79 // Remote Source Route Bridging - IfTypeAtmLogical = 80 // ATM Logical Port - IfTypeDs0 = 81 // Digital Signal Level 0 - IfTypeDs0Bundle = 82 // Group of ds0s on the same ds1 - IfTypeBsc = 83 // Bisynchronous Protocol - IfTypeAsync = 84 // Asynchronous Protocol - IfTypeCnr = 85 // Combat Net Radio - IfTypeIso88025rDtr = 86 // ISO 802.5r DTR - IfTypeEplrs = 87 // Ext Pos Loc Report Sys - IfTypeArap = 88 // Appletalk Remote Access Protocol - IfTypePropCnls = 89 // Proprietary Connectionless Proto - IfTypeHostpad = 90 // CCITT-ITU X.29 PAD Protocol - IfTypeTermpad = 91 // CCITT-ITU X.3 PAD Facility - IfTypeFramerelayMpi = 92 // Multiproto Interconnect over FR - IfTypeX213 = 93 // CCITT-ITU X213 - IfTypeAdsl = 94 // Asymmetric Digital Subscrbr Loop - IfTypeRadsl = 95 // Rate-Adapt Digital Subscrbr Loop - IfTypeSdsl = 96 // Symmetric Digital Subscriber Loop - IfTypeVdsl = 97 // Very H-Speed Digital Subscrb Loop - IfTypeIso88025Crfprint = 98 // ISO 802.5 CRFP - IfTypeMyrinet = 99 // Myricom Myrinet - IfTypeVoiceEm = 100 // Voice recEive and transMit - IfTypeVoiceFxo = 101 // Voice Foreign Exchange Office - IfTypeVoiceFxs = 102 // Voice Foreign Exchange Station - IfTypeVoiceEncap = 103 // Voice encapsulation - IfTypeVoiceOverip = 104 // Voice over IP encapsulation - IfTypeAtmDxi = 105 // ATM DXI - IfTypeAtmFuni = 106 // ATM FUNI - IfTypeAtmIma = 107 // ATM IMA - IfTypePPPmultilinkbundle = 108 // PPP Multilink Bundle - IfTypeIpoverCdlc = 109 // IBM ipOverCdlc - IfTypeIpoverClaw = 110 // IBM Common Link Access to Workstn - IfTypeStacktostack = 111 // IBM stackToStack - IfTypeVirtualipaddress = 112 // IBM VIPA - IfTypeMpc = 113 // IBM multi-proto channel support - IfTypeIpoverAtm = 114 // IBM ipOverAtm - IfTypeIso88025Fiber = 115 // ISO 802.5j Fiber Token Ring - IfTypeTdlc = 116 // IBM twinaxial data link control - IfTypeGigabitethernet = 117 - IfTypeHdlc = 118 - IfTypeLapF = 119 - IfTypeV37 = 120 - IfTypeX25Mlp = 121 // Multi-Link Protocol - IfTypeX25Huntgroup = 122 // X.25 Hunt Group - IfTypeTransphdlc = 123 - IfTypeInterleave = 124 // Interleave channel - IfTypeFast = 125 // Fast channel - IfTypeIP = 126 // IP (for APPN HPR in IP networks) - IfTypeDocscableMaclayer = 127 // CATV Mac Layer - IfTypeDocscableDownstream = 128 // CATV Downstream interface - IfTypeDocscableUpstream = 129 // CATV Upstream interface - IfTypeA12mppswitch = 130 // Avalon Parallel Processor - IfTypeTunnel = 131 // Encapsulation interface - IfTypeCoffee = 132 // Coffee pot - IfTypeCes = 133 // Circuit Emulation Service - IfTypeAtmSubinterface = 134 // ATM Sub Interface - IfTypeL2Vlan = 135 // Layer 2 Virtual LAN using 802.1Q - IfTypeL3Ipvlan = 136 // Layer 3 Virtual LAN using IP - IfTypeL3Ipxvlan = 137 // Layer 3 Virtual LAN using IPX - IfTypeDigitalpowerline = 138 // IP over Power Lines - IfTypeMediamailoverip = 139 // Multimedia Mail over IP - IfTypeDtm = 140 // Dynamic syncronous Transfer Mode - IfTypeDcn = 141 // Data Communications Network - IfTypeIpforward = 142 // IP Forwarding Interface - IfTypeMsdsl = 143 // Multi-rate Symmetric DSL - IfTypeIEEE1394 = 144 // IEEE1394 High Perf Serial Bus - IfTypeIfGsn = 145 - IfTypeDvbrccMaclayer = 146 - IfTypeDvbrccDownstream = 147 - IfTypeDvbrccUpstream = 148 - IfTypeAtmVirtual = 149 - IfTypeMplsTunnel = 150 - IfTypeSrp = 151 - IfTypeVoiceoveratm = 152 - IfTypeVoiceoverframerelay = 153 - IfTypeIdsl = 154 - IfTypeCompositelink = 155 - IfTypeSs7Siglink = 156 - IfTypePropWirelessP2P = 157 - IfTypeFrForward = 158 - IfTypeRfc1483 = 159 - IfTypeUsb = 160 - IfTypeIEEE8023adLag = 161 - IfTypeBgpPolicyAccounting = 162 - IfTypeFrf16MfrBundle = 163 - IfTypeH323Gatekeeper = 164 - IfTypeH323Proxy = 165 - IfTypeMpls = 166 - IfTypeMfSiglink = 167 - IfTypeHdsl2 = 168 - IfTypeShdsl = 169 - IfTypeDs1Fdl = 170 - IfTypePos = 171 - IfTypeDvbAsiIn = 172 - IfTypeDvbAsiOut = 173 - IfTypePlc = 174 - IfTypeNfas = 175 - IfTypeTr008 = 176 - IfTypeGr303Rdt = 177 - IfTypeGr303Idt = 178 - IfTypeIsup = 179 - IfTypePropDocsWirelessMaclayer = 180 - IfTypePropDocsWirelessDownstream = 181 - IfTypePropDocsWirelessUpstream = 182 - IfTypeHiperlan2 = 183 - IfTypePropBwaP2MP = 184 - IfTypeSonetOverheadChannel = 185 - IfTypeDigitalWrapperOverheadChannel = 186 - IfTypeAal2 = 187 - IfTypeRadioMac = 188 - IfTypeAtmRadio = 189 - IfTypeImt = 190 - IfTypeMvl = 191 - IfTypeReachDsl = 192 - IfTypeFrDlciEndpt = 193 - IfTypeAtmVciEndpt = 194 - IfTypeOpticalChannel = 195 - IfTypeOpticalTransport = 196 - IfTypeIEEE80216Wman = 237 - IfTypeWwanpp = 243 // WWAN devices based on GSM technology - IfTypeWwanpp2 = 244 // WWAN devices based on CDMA technology - IfTypeIEEE802154 = 259 // IEEE 802.15.4 WPAN interface - IfTypeXboxWireless = 281 + IfTypeRegular1822 IfType = 2 + IfTypeHdh1822 IfType = 3 + IfTypeDdnX25 IfType = 4 + IfTypeRfc877X25 IfType = 5 + IfTypeEthernetCSMACD IfType = 6 + IfTypeISO88023CSMACD IfType = 7 + IfTypeISO88024Tokenbus IfType = 8 + IfTypeISO88025Tokenring IfType = 9 + IfTypeISO88026Man IfType = 10 + IfTypeStarlan IfType = 11 + IfTypeProteon10Mbit IfType = 12 + IfTypeProteon80Mbit IfType = 13 + IfTypeHyperchannel IfType = 14 + IfTypeFddi IfType = 15 + IfTypeLapB IfType = 16 + IfTypeSdlc IfType = 17 + IfTypeDs1 IfType = 18 // DS1-MIB + IfTypeE1 IfType = 19 // Obsolete; see DS1-MIB + IfTypeBasicISDN IfType = 20 + IfTypePrimaryISDN IfType = 21 + IfTypePropPoint2PointSerial IfType = 22 // proprietary serial + IfTypePPP IfType = 23 + IfTypeSoftwareLoopback IfType = 24 + IfTypeEon IfType = 25 // CLNP over IP + IfTypeEthernet3Mbit IfType = 26 + IfTypeNsip IfType = 27 // XNS over IP + IfTypeSlip IfType = 28 // Generic Slip + IfTypeUltra IfType = 29 // ULTRA Technologies + IfTypeDs3 IfType = 30 // DS3-MIB + IfTypeSip IfType = 31 // SMDS, coffee + IfTypeFramerelay IfType = 32 // DTE only + IfTypeRs232 IfType = 33 + IfTypePara IfType = 34 // Parallel port + IfTypeArcnet IfType = 35 + IfTypeArcnetPlus IfType = 36 + IfTypeAtm IfType = 37 // ATM cells + IfTypeMioX25 IfType = 38 + IfTypeSonet IfType = 39 // SONET or SDH + IfTypeX25Ple IfType = 40 + IfTypeIso88022LLC IfType = 41 + IfTypeLocaltalk IfType = 42 + IfTypeSmdsDxi IfType = 43 + IfTypeFramerelayService IfType = 44 // FRNETSERV-MIB + IfTypeV35 IfType = 45 + IfTypeHssi IfType = 46 + IfTypeHippi IfType = 47 + IfTypeModem IfType = 48 // Generic Modem + IfTypeAal5 IfType = 49 // AAL5 over ATM + IfTypeSonetPath IfType = 50 + IfTypeSonetVt IfType = 51 + IfTypeSmdsIcip IfType = 52 // SMDS InterCarrier Interface + IfTypePropVirtual IfType = 53 // Proprietary virtual/internal + IfTypePropMultiplexor IfType = 54 // Proprietary multiplexing + IfTypeIEEE80212 IfType = 55 // 100BaseVG + IfTypeFibrechannel IfType = 56 + IfTypeHippiinterface IfType = 57 + IfTypeFramerelayInterconnect IfType = 58 // Obsolete, use 32 or 44 + IfTypeAflane8023 IfType = 59 // ATM Emulated LAN for 802.3 + IfTypeAflane8025 IfType = 60 // ATM Emulated LAN for 802.5 + IfTypeCctemul IfType = 61 // ATM Emulated circuit + IfTypeFastether IfType = 62 // Fast Ethernet (100BaseT) + IfTypeISDN IfType = 63 // ISDN and X.25 + IfTypeV11 IfType = 64 // CCITT V.11/X.21 + IfTypeV36 IfType = 65 // CCITT V.36 + IfTypeG703_64k IfType = 66 // CCITT G703 at 64Kbps + IfTypeG703_2mb IfType = 67 // Obsolete; see DS1-MIB + IfTypeQllc IfType = 68 // SNA QLLC + IfTypeFastetherFX IfType = 69 // Fast Ethernet (100BaseFX) + IfTypeChannel IfType = 70 + IfTypeIEEE80211 IfType = 71 // Radio spread spectrum + IfTypeIBM370parchan IfType = 72 // IBM System 360/370 OEMI Channel + IfTypeEscon IfType = 73 // IBM Enterprise Systems Connection + IfTypeDlsw IfType = 74 // Data Link Switching + IfTypeISDNS IfType = 75 // ISDN S/T interface + IfTypeISDNU IfType = 76 // ISDN U interface + IfTypeLapD IfType = 77 // Link Access Protocol D + IfTypeIpswitch IfType = 78 // IP Switching Objects + IfTypeRsrb IfType = 79 // Remote Source Route Bridging + IfTypeAtmLogical IfType = 80 // ATM Logical Port + IfTypeDs0 IfType = 81 // Digital Signal Level 0 + IfTypeDs0Bundle IfType = 82 // Group of ds0s on the same ds1 + IfTypeBsc IfType = 83 // Bisynchronous Protocol + IfTypeAsync IfType = 84 // Asynchronous Protocol + IfTypeCnr IfType = 85 // Combat Net Radio + IfTypeIso88025rDtr IfType = 86 // ISO 802.5r DTR + IfTypeEplrs IfType = 87 // Ext Pos Loc Report Sys + IfTypeArap IfType = 88 // Appletalk Remote Access Protocol + IfTypePropCnls IfType = 89 // Proprietary Connectionless Proto + IfTypeHostpad IfType = 90 // CCITT-ITU X.29 PAD Protocol + IfTypeTermpad IfType = 91 // CCITT-ITU X.3 PAD Facility + IfTypeFramerelayMpi IfType = 92 // Multiproto Interconnect over FR + IfTypeX213 IfType = 93 // CCITT-ITU X213 + IfTypeAdsl IfType = 94 // Asymmetric Digital Subscrbr Loop + IfTypeRadsl IfType = 95 // Rate-Adapt Digital Subscrbr Loop + IfTypeSdsl IfType = 96 // Symmetric Digital Subscriber Loop + IfTypeVdsl IfType = 97 // Very H-Speed Digital Subscrb Loop + IfTypeIso88025Crfprint IfType = 98 // ISO 802.5 CRFP + IfTypeMyrinet IfType = 99 // Myricom Myrinet + IfTypeVoiceEm IfType = 100 // Voice recEive and transMit + IfTypeVoiceFxo IfType = 101 // Voice Foreign Exchange Office + IfTypeVoiceFxs IfType = 102 // Voice Foreign Exchange Station + IfTypeVoiceEncap IfType = 103 // Voice encapsulation + IfTypeVoiceOverip IfType = 104 // Voice over IP encapsulation + IfTypeAtmDxi IfType = 105 // ATM DXI + IfTypeAtmFuni IfType = 106 // ATM FUNI + IfTypeAtmIma IfType = 107 // ATM IMA + IfTypePPPmultilinkbundle IfType = 108 // PPP Multilink Bundle + IfTypeIpoverCdlc IfType = 109 // IBM ipOverCdlc + IfTypeIpoverClaw IfType = 110 // IBM Common Link Access to Workstn + IfTypeStacktostack IfType = 111 // IBM stackToStack + IfTypeVirtualipaddress IfType = 112 // IBM VIPA + IfTypeMpc IfType = 113 // IBM multi-proto channel support + IfTypeIpoverAtm IfType = 114 // IBM ipOverAtm + IfTypeIso88025Fiber IfType = 115 // ISO 802.5j Fiber Token Ring + IfTypeTdlc IfType = 116 // IBM twinaxial data link control + IfTypeGigabitethernet IfType = 117 + IfTypeHdlc IfType = 118 + IfTypeLapF IfType = 119 + IfTypeV37 IfType = 120 + IfTypeX25Mlp IfType = 121 // Multi-Link Protocol + IfTypeX25Huntgroup IfType = 122 // X.25 Hunt Group + IfTypeTransphdlc IfType = 123 + IfTypeInterleave IfType = 124 // Interleave channel + IfTypeFast IfType = 125 // Fast channel + IfTypeIP IfType = 126 // IP (for APPN HPR in IP networks) + IfTypeDocscableMaclayer IfType = 127 // CATV Mac Layer + IfTypeDocscableDownstream IfType = 128 // CATV Downstream interface + IfTypeDocscableUpstream IfType = 129 // CATV Upstream interface + IfTypeA12mppswitch IfType = 130 // Avalon Parallel Processor + IfTypeTunnel IfType = 131 // Encapsulation interface + IfTypeCoffee IfType = 132 // Coffee pot + IfTypeCes IfType = 133 // Circuit Emulation Service + IfTypeAtmSubinterface IfType = 134 // ATM Sub Interface + IfTypeL2Vlan IfType = 135 // Layer 2 Virtual LAN using 802.1Q + IfTypeL3Ipvlan IfType = 136 // Layer 3 Virtual LAN using IP + IfTypeL3Ipxvlan IfType = 137 // Layer 3 Virtual LAN using IPX + IfTypeDigitalpowerline IfType = 138 // IP over Power Lines + IfTypeMediamailoverip IfType = 139 // Multimedia Mail over IP + IfTypeDtm IfType = 140 // Dynamic syncronous Transfer Mode + IfTypeDcn IfType = 141 // Data Communications Network + IfTypeIpforward IfType = 142 // IP Forwarding Interface + IfTypeMsdsl IfType = 143 // Multi-rate Symmetric DSL + IfTypeIEEE1394 IfType = 144 // IEEE1394 High Perf Serial Bus + IfTypeIfGsn IfType = 145 + IfTypeDvbrccMaclayer IfType = 146 + IfTypeDvbrccDownstream IfType = 147 + IfTypeDvbrccUpstream IfType = 148 + IfTypeAtmVirtual IfType = 149 + IfTypeMplsTunnel IfType = 150 + IfTypeSrp IfType = 151 + IfTypeVoiceoveratm IfType = 152 + IfTypeVoiceoverframerelay IfType = 153 + IfTypeIdsl IfType = 154 + IfTypeCompositelink IfType = 155 + IfTypeSs7Siglink IfType = 156 + IfTypePropWirelessP2P IfType = 157 + IfTypeFrForward IfType = 158 + IfTypeRfc1483 IfType = 159 + IfTypeUsb IfType = 160 + IfTypeIEEE8023adLag IfType = 161 + IfTypeBgpPolicyAccounting IfType = 162 + IfTypeFrf16MfrBundle IfType = 163 + IfTypeH323Gatekeeper IfType = 164 + IfTypeH323Proxy IfType = 165 + IfTypeMpls IfType = 166 + IfTypeMfSiglink IfType = 167 + IfTypeHdsl2 IfType = 168 + IfTypeShdsl IfType = 169 + IfTypeDs1Fdl IfType = 170 + IfTypePos IfType = 171 + IfTypeDvbAsiIn IfType = 172 + IfTypeDvbAsiOut IfType = 173 + IfTypePlc IfType = 174 + IfTypeNfas IfType = 175 + IfTypeTr008 IfType = 176 + IfTypeGr303Rdt IfType = 177 + IfTypeGr303Idt IfType = 178 + IfTypeIsup IfType = 179 + IfTypePropDocsWirelessMaclayer IfType = 180 + IfTypePropDocsWirelessDownstream IfType = 181 + IfTypePropDocsWirelessUpstream IfType = 182 + IfTypeHiperlan2 IfType = 183 + IfTypePropBwaP2MP IfType = 184 + IfTypeSonetOverheadChannel IfType = 185 + IfTypeDigitalWrapperOverheadChannel IfType = 186 + IfTypeAal2 IfType = 187 + IfTypeRadioMac IfType = 188 + IfTypeAtmRadio IfType = 189 + IfTypeImt IfType = 190 + IfTypeMvl IfType = 191 + IfTypeReachDsl IfType = 192 + IfTypeFrDlciEndpt IfType = 193 + IfTypeAtmVciEndpt IfType = 194 + IfTypeOpticalChannel IfType = 195 + IfTypeOpticalTransport IfType = 196 + IfTypeIEEE80216Wman IfType = 237 + IfTypeWwanpp IfType = 243 // WWAN devices based on GSM technology + IfTypeWwanpp2 IfType = 244 // WWAN devices based on CDMA technology + IfTypeIEEE802154 IfType = 259 // IEEE 802.15.4 WPAN interface + IfTypeXboxWireless IfType = 281 ) // MibIfEntryLevel enumeration specifies level of interface information to retrieve in GetIfTable2Ex function call. @@ -270,7 +272,7 @@ type MibIfEntryLevel uint32 const ( MibIfEntryNormal MibIfEntryLevel = 0 - MibIfEntryNormalWithoutStatistics = 2 + MibIfEntryNormalWithoutStatistics MibIfEntryLevel = 2 ) // NdisMedium enumeration type identifies the medium types that NDIS drivers support. @@ -522,12 +524,12 @@ type TunnelType uint32 const ( TunnelTypeNone TunnelType = 0 - TunnelTypeOther = 1 - TunnelTypeDirect = 2 - TunnelType6to4 = 11 - TunnelTypeIsatap = 13 - TunnelTypeTeredo = 14 - TunnelTypeIPHTTPS = 15 + TunnelTypeOther TunnelType = 1 + TunnelTypeDirect TunnelType = 2 + TunnelType6to4 TunnelType = 11 + TunnelTypeIsatap TunnelType = 13 + TunnelTypeTeredo TunnelType = 14 + TunnelTypeIPHTTPS TunnelType = 15 ) // InterfaceAndOperStatusFlags enumeration type defines interface and operation flags @@ -574,13 +576,13 @@ type ScopeLevel uint32 const ( ScopeLevelInterface ScopeLevel = 1 - ScopeLevelLink = 2 - ScopeLevelSubnet = 3 - ScopeLevelAdmin = 4 - ScopeLevelSite = 5 - ScopeLevelOrganization = 8 - ScopeLevelGlobal = 14 - ScopeLevelCount = 16 + ScopeLevelLink ScopeLevel = 2 + ScopeLevelSubnet ScopeLevel = 3 + ScopeLevelAdmin ScopeLevel = 4 + ScopeLevelSite ScopeLevel = 5 + ScopeLevelOrganization ScopeLevel = 8 + ScopeLevelGlobal ScopeLevel = 14 + ScopeLevelCount ScopeLevel = 16 ) // RouteData structure describes a route to add @@ -757,7 +759,7 @@ func (addr *RawSockaddrInet) SetAddrPort(addrPort netip.AddrPort) error { addr4.Family = windows.AF_INET addr4.Addr = addrPort.Addr().As4() addr4.Port = htons(addrPort.Port()) - for i := 0; i < 8; i++ { + for i := range 8 { addr4.Zero[i] = 0 } return nil diff --git a/internal/winipcfg/types_32.go b/internal/winipcfg/types_32.go index 1a8d4443..bac06baa 100644 --- a/internal/winipcfg/types_32.go +++ b/internal/winipcfg/types_32.go @@ -1,4 +1,4 @@ -//go:build 386 || arm +//go:build windows && (386 || arm) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/types_64.go b/internal/winipcfg/types_64.go index 3a1fe07f..13d3ab91 100644 --- a/internal/winipcfg/types_64.go +++ b/internal/winipcfg/types_64.go @@ -1,4 +1,4 @@ -//go:build amd64 || arm64 +//go:build windows && (amd64 || arm64) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/types_test.go b/internal/winipcfg/types_test.go index b72d73f5..f51c8c96 100644 --- a/internal/winipcfg/types_test.go +++ b/internal/winipcfg/types_test.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/types_test_32.go b/internal/winipcfg/types_test_32.go index 9e62bfef..b6e10923 100644 --- a/internal/winipcfg/types_test_32.go +++ b/internal/winipcfg/types_test_32.go @@ -1,4 +1,4 @@ -//go:build 386 || arm +//go:build windows && (386 || arm) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/types_test_64.go b/internal/winipcfg/types_test_64.go index 8a181575..f94a88dd 100644 --- a/internal/winipcfg/types_test_64.go +++ b/internal/winipcfg/types_test_64.go @@ -1,4 +1,4 @@ -//go:build amd64 || arm64 +//go:build windows && (amd64 || arm64) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/unicast_address_change_handler.go b/internal/winipcfg/unicast_address_change_handler.go index cf4fcb3a..0d80f1d1 100644 --- a/internal/winipcfg/unicast_address_change_handler.go +++ b/internal/winipcfg/unicast_address_change_handler.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/winipcfg.go b/internal/winipcfg/winipcfg.go index e24157b9..7a460d77 100644 --- a/internal/winipcfg/winipcfg.go +++ b/internal/winipcfg/winipcfg.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/winipcfg_test.go b/internal/winipcfg/winipcfg_test.go index b49daf33..1cba10b1 100644 --- a/internal/winipcfg/winipcfg_test.go +++ b/internal/winipcfg/winipcfg_test.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. @@ -104,6 +106,9 @@ func TestAdaptersAddresses(t *testing.T) { } ifcs, err = GetAdaptersAddresses(windows.AF_UNSPEC, GAAFlagDefault) + if err != nil { + t.Errorf("GetAdaptersAddresses() returned error: %v", err) + } for _, i := range ifcs { ifc, err := i.LUID.Interface() @@ -370,7 +375,7 @@ func TestAddDeleteIPAddress(t *testing.T) { return } - addr, err := ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) + _, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) if err == nil { t.Errorf("Unicast address %s already exists. Please set nonexistantIPv4ToAdd appropriately.", nonexistantIPv4ToAdd.Addr().String()) return @@ -414,7 +419,7 @@ func TestAddDeleteIPAddress(t *testing.T) { if count != 1 { t.Errorf("After adding there are %d new interface(s).", count) } - addr, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) + addr, err := ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) if err != nil { t.Errorf("LUID.IPAddress() returned an error: %w", err) } else if addr == nil { @@ -431,7 +436,7 @@ func TestAddDeleteIPAddress(t *testing.T) { time.Sleep(500 * time.Millisecond) - addr, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) + _, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) if err == nil { t.Errorf("Unicast address %s still exists, although it's deleted successfully.", nonexistantIPv4ToAdd.Addr().String()) } else if err != windows.ERROR_NOT_FOUND { diff --git a/internal/wintun/memmod/memmod_windows.go b/internal/wintun/memmod/memmod_windows.go index 985d48a1..0b345ba6 100644 --- a/internal/wintun/memmod/memmod_windows.go +++ b/internal/wintun/memmod/memmod_windows.go @@ -56,16 +56,16 @@ func (module *Module) copySections(address, size uintptr, oldHeaders *IMAGE_NT_H if sectionSize == 0 { continue } - dest, err := windows.VirtualAlloc(module.codeBase+uintptr(sections[i].VirtualAddress), + _, err := windows.VirtualAlloc(module.codeBase+uintptr(sections[i].VirtualAddress), uintptr(sectionSize), windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - return fmt.Errorf("Error allocating section: %w", err) + return fmt.Errorf("error allocating section: %w", err) } // Always use position from file to support alignments smaller than page size (allocation above will align to page size). - dest = module.codeBase + uintptr(sections[i].VirtualAddress) + dest := module.codeBase + uintptr(sections[i].VirtualAddress) // NOTE: On 64bit systems we truncate to 32bit here but expand again later when "PhysicalAddress" is used. sections[i].SetPhysicalAddress((uint32)(dest & 0xffffffff)) dst := unsafe.Slice((*byte)(a2p(dest)), sectionSize) @@ -76,7 +76,7 @@ func (module *Module) copySections(address, size uintptr, oldHeaders *IMAGE_NT_H } if size < uintptr(sections[i].PointerToRawData)+uintptr(sections[i].SizeOfRawData) { - return errors.New("Incomplete section") + return errors.New("incomplete section") } // Commit memory block and copy data from dll. @@ -85,7 +85,7 @@ func (module *Module) copySections(address, size uintptr, oldHeaders *IMAGE_NT_H windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - return fmt.Errorf("Error allocating memory block: %w", err) + return fmt.Errorf("error allocating memory block: %w", err) } // Always use position from file to support alignments smaller than page size (allocation above will align to page size). @@ -158,7 +158,7 @@ func (module *Module) finalizeSection(sectionData *sectionFinalizeData) error { var oldProtect uint32 err := windows.VirtualProtect(sectionData.address, sectionData.size, protect, &oldProtect) if err != nil { - return fmt.Errorf("Error protecting memory page: %w", err) + return fmt.Errorf("error protecting memory page: %w", err) } return nil @@ -204,7 +204,7 @@ func (module *Module) finalizeSections() error { err := module.finalizeSection(§ionData) if err != nil { - return fmt.Errorf("Error finalizing section: %w", err) + return fmt.Errorf("error finalizing section: %w", err) } sectionData.address = sectionAddress sectionData.alignedAddress = alignedAddress @@ -214,7 +214,7 @@ func (module *Module) finalizeSections() error { sectionData.last = true err := module.finalizeSection(§ionData) if err != nil { - return fmt.Errorf("Error finalizing section: %w", err) + return fmt.Errorf("error finalizing section: %w", err) } return nil } @@ -250,10 +250,10 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err relocationHdr := (*IMAGE_BASE_RELOCATION)(a2p(relocBase)) for uintptr(unsafe.Pointer(relocationHdr))+unsafe.Sizeof(*relocationHdr) <= relocEnd && relocationHdr.VirtualAddress > 0 { if uintptr(relocationHdr.SizeOfBlock) < unsafe.Sizeof(*relocationHdr) { - return false, errors.New("Invalid relocation block size") + return false, errors.New("invalid relocation block size") } if uintptr(unsafe.Pointer(relocationHdr))+uintptr(relocationHdr.SizeOfBlock) > relocEnd { - return false, errors.New("Relocation block exceeds directory bounds") + return false, errors.New("relocation block exceeds directory bounds") } dest := module.codeBase + uintptr(relocationHdr.VirtualAddress) @@ -272,11 +272,9 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err case IMAGE_REL_BASED_LOW: *(*uint16)(a2p(dest + relOffset)) += uint16(delta & 0xffff) - break case IMAGE_REL_BASED_HIGH: *(*uint16)(a2p(dest + relOffset)) += uint16(uint32(delta) >> 16) - break case IMAGE_REL_BASED_HIGHLOW: *(*uint32)(a2p(dest + relOffset)) += uint32(delta) @@ -289,7 +287,7 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err imm16 := ((inst << 1) & 0x0800) + ((inst << 12) & 0xf000) + ((inst >> 20) & 0x0700) + ((inst >> 16) & 0x00ff) if (inst & 0x8000fbf0) != 0x0000f240 { - return false, fmt.Errorf("Wrong Thumb2 instruction %08x, expected MOVW", inst) + return false, fmt.Errorf("wrong Thumb2 instruction %08x, expected MOVW", inst) } imm16 += uint32(delta) & 0xffff hiDelta := (uint32(delta&0xffff0000) >> 16) + ((imm16 & 0xffff0000) >> 16) @@ -302,11 +300,11 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err imm16 = ((inst << 1) & 0x0800) + ((inst << 12) & 0xf000) + ((inst >> 20) & 0x0700) + ((inst >> 16) & 0x00ff) if (inst & 0x8000fbf0) != 0x0000f2c0 { - return false, fmt.Errorf("Wrong Thumb2 instruction %08x, expected MOVT", inst) + return false, fmt.Errorf("wrong Thumb2 instruction %08x, expected MOVT", inst) } imm16 += hiDelta if imm16 > 0xffff { - return false, fmt.Errorf("Resulting immediate value won't fit: %08x", imm16) + return false, fmt.Errorf("resulting immediate value won't fit: %08x", imm16) } *(*uint32)(a2p(dest + relOffset + 4)) = (inst & 0x8f00fbf0) + ((imm16 >> 1) & 0x0400) + @@ -316,7 +314,7 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err } default: - return false, fmt.Errorf("Unsupported relocation: %v", relType) + return false, fmt.Errorf("unsupported relocation: %v", relType) } } @@ -337,7 +335,7 @@ func (module *Module) buildImportTable() error { for importDesc.Name != 0 { handle, err := windows.LoadLibraryEx(windows.BytePtrToString((*byte)(a2p(module.codeBase+uintptr(importDesc.Name)))), 0, windows.LOAD_LIBRARY_SEARCH_SYSTEM32) if err != nil { - return fmt.Errorf("Error loading module: %w", err) + return fmt.Errorf("error loading module: %w", err) } var thunkRef, funcRef *uintptr if importDesc.OriginalFirstThunk() != 0 { @@ -357,7 +355,7 @@ func (module *Module) buildImportTable() error { } if err != nil { windows.FreeLibrary(handle) - return fmt.Errorf("Error getting function address: %w", err) + return fmt.Errorf("error getting function address: %w", err) } thunkRef = (*uintptr)(a2p(uintptr(unsafe.Pointer(thunkRef)) + unsafe.Sizeof(*thunkRef))) funcRef = (*uintptr)(a2p(uintptr(unsafe.Pointer(funcRef)) + unsafe.Sizeof(*funcRef))) @@ -371,14 +369,14 @@ func (module *Module) buildImportTable() error { func (module *Module) buildNameExports() error { directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT) if directory.Size == 0 { - return errors.New("No export table found") + return errors.New("no export table found") } exports := (*IMAGE_EXPORT_DIRECTORY)(a2p(module.codeBase + uintptr(directory.VirtualAddress))) if exports.NumberOfNames == 0 || exports.NumberOfFunctions == 0 { - return errors.New("No functions exported") + return errors.New("no functions exported") } if exports.NumberOfNames == 0 { - return errors.New("No functions exported by name") + return errors.New("no functions exported by name") } nameRefs := unsafe.Slice((*uint32)(a2p(module.codeBase+uintptr(exports.AddressOfNames))), exports.NumberOfNames) ordinals := unsafe.Slice((*uint16)(a2p(module.codeBase+uintptr(exports.AddressOfNameOrdinals))), exports.NumberOfNames) @@ -466,39 +464,39 @@ func hookRtlPcToFileHeader() error { func LoadLibrary(data []byte) (module *Module, err error) { size := uintptr(len(data)) if size < unsafe.Sizeof(IMAGE_DOS_HEADER{}) { - return nil, errors.New("Incomplete IMAGE_DOS_HEADER") + return nil, errors.New("incomplete IMAGE_DOS_HEADER") } addr := uintptr(unsafe.Pointer(&data[0])) dosHeader := (*IMAGE_DOS_HEADER)(a2p(addr)) if dosHeader.E_magic != IMAGE_DOS_SIGNATURE { - return nil, fmt.Errorf("Not an MS-DOS binary (provided: %x, expected: %x)", dosHeader.E_magic, IMAGE_DOS_SIGNATURE) + return nil, fmt.Errorf("not an MS-DOS binary (provided: %x, expected: %x)", dosHeader.E_magic, IMAGE_DOS_SIGNATURE) } if dosHeader.E_lfanew < 0 || (size < uintptr(dosHeader.E_lfanew)+unsafe.Sizeof(IMAGE_NT_HEADERS{})) { - return nil, errors.New("Incomplete IMAGE_NT_HEADERS") + return nil, errors.New("incomplete IMAGE_NT_HEADERS") } oldHeader := (*IMAGE_NT_HEADERS)(a2p(addr + uintptr(dosHeader.E_lfanew))) if oldHeader.Signature != IMAGE_NT_SIGNATURE { - return nil, fmt.Errorf("Not an NT binary (provided: %x, expected: %x)", oldHeader.Signature, IMAGE_NT_SIGNATURE) + return nil, fmt.Errorf("not an NT binary (provided: %x, expected: %x)", oldHeader.Signature, IMAGE_NT_SIGNATURE) } if oldHeader.FileHeader.Machine != imageFileProcess { - return nil, fmt.Errorf("Foreign platform (provided: %x, expected: %x)", oldHeader.FileHeader.Machine, imageFileProcess) + return nil, fmt.Errorf("foreign platform (provided: %x, expected: %x)", oldHeader.FileHeader.Machine, imageFileProcess) } if oldHeader.OptionalHeader.SectionAlignment == 0 || (oldHeader.OptionalHeader.SectionAlignment&(oldHeader.OptionalHeader.SectionAlignment-1)) != 0 { - return nil, errors.New("Unaligned section") + return nil, errors.New("unaligned section") } if oldHeader.FileHeader.NumberOfSections == 0 { - return nil, errors.New("No sections") + return nil, errors.New("no sections") } if uintptr(oldHeader.FileHeader.SizeOfOptionalHeader) < unsafe.Sizeof(oldHeader.OptionalHeader) { - return nil, errors.New("Incomplete optional header") + return nil, errors.New("incomplete optional header") } if oldHeader.OptionalHeader.NumberOfRvaAndSizes < IMAGE_NUMBEROF_DIRECTORY_ENTRIES { - return nil, errors.New("Incomplete data directory") + return nil, errors.New("incomplete data directory") } sectionHeadersEnd := uintptr(dosHeader.E_lfanew) + unsafe.Offsetof(oldHeader.OptionalHeader) + uintptr(oldHeader.FileHeader.SizeOfOptionalHeader) + uintptr(oldHeader.FileHeader.NumberOfSections)*unsafe.Sizeof(IMAGE_SECTION_HEADER{}) if size < sectionHeadersEnd { - return nil, errors.New("Incomplete section headers") + return nil, errors.New("incomplete section headers") } lastSectionEnd := uintptr(0) sections := oldHeader.Sections() @@ -517,7 +515,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { } alignedImageSize := alignUp(uintptr(oldHeader.OptionalHeader.SizeOfImage), uintptr(oldHeader.OptionalHeader.SectionAlignment)) if alignedImageSize != alignUp(lastSectionEnd, uintptr(oldHeader.OptionalHeader.SectionAlignment)) { - return nil, errors.New("Section is not page-aligned") + return nil, errors.New("section is not page-aligned") } module = &Module{isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0} @@ -541,18 +539,18 @@ func LoadLibrary(data []byte) (module *Module, err error) { windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - err = fmt.Errorf("Error allocating code: %w", err) + err = fmt.Errorf("error allocating code: %w", err) return } } err = module.check4GBBoundaries(alignedImageSize) if err != nil { - err = fmt.Errorf("Error reallocating code: %w", err) + err = fmt.Errorf("error reallocating code: %w", err) return } if size < uintptr(oldHeader.OptionalHeader.SizeOfHeaders) { - err = errors.New("Incomplete headers") + err = errors.New("incomplete headers") return } // Commit memory for headers. @@ -561,7 +559,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - err = fmt.Errorf("Error allocating headers: %w", err) + err = fmt.Errorf("error allocating headers: %w", err) return } // Copy PE header to code. @@ -574,7 +572,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { // Copy sections from DLL file block to new memory location. err = module.copySections(addr, size, oldHeader) if err != nil { - err = fmt.Errorf("Error copying sections: %w", err) + err = fmt.Errorf("error copying sections: %w", err) return } @@ -583,7 +581,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { if locationDelta != 0 { module.isRelocated, err = module.performBaseRelocation(locationDelta) if err != nil { - err = fmt.Errorf("Error relocating module: %w", err) + err = fmt.Errorf("error relocating module: %w", err) return } if !module.isRelocated { @@ -597,14 +595,14 @@ func LoadLibrary(data []byte) (module *Module, err error) { // Load required dlls and adjust function table of imports. err = module.buildImportTable() if err != nil { - err = fmt.Errorf("Error building import table: %w", err) + err = fmt.Errorf("error building import table: %w", err) return } // Mark memory pages depending on section headers and release sections that are marked as "discardable". err = module.finalizeSections() if err != nil { - err = fmt.Errorf("Error finalizing sections: %w", err) + err = fmt.Errorf("error finalizing sections: %w", err) return } @@ -673,35 +671,35 @@ func (module *Module) Free() { func (module *Module) ProcAddressByName(name string) (uintptr, error) { directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT) if directory.Size == 0 { - return 0, errors.New("No export table found") + return 0, errors.New("no export table found") } exports := (*IMAGE_EXPORT_DIRECTORY)(a2p(module.codeBase + uintptr(directory.VirtualAddress))) if module.nameExports == nil { - return 0, errors.New("No functions exported by name") + return 0, errors.New("no functions exported by name") } if idx, ok := module.nameExports[name]; ok { if uint32(idx) >= exports.NumberOfFunctions { - return 0, errors.New("Ordinal number too high") + return 0, errors.New("ordinal number too high") } // AddressOfFunctions contains the RVAs to the "real" functions. return module.codeBase + uintptr(*(*uint32)(a2p(module.codeBase + uintptr(exports.AddressOfFunctions) + uintptr(idx)*4))), nil } - return 0, errors.New("Function not found by name") + return 0, errors.New("function not found by name") } // ProcAddressByOrdinal returns function address by exported ordinal. func (module *Module) ProcAddressByOrdinal(ordinal uint16) (uintptr, error) { directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT) if directory.Size == 0 { - return 0, errors.New("No export table found") + return 0, errors.New("no export table found") } exports := (*IMAGE_EXPORT_DIRECTORY)(a2p(module.codeBase + uintptr(directory.VirtualAddress))) if uint32(ordinal) < exports.Base { - return 0, errors.New("Ordinal number too low") + return 0, errors.New("ordinal number too low") } idx := ordinal - uint16(exports.Base) if uint32(idx) >= exports.NumberOfFunctions { - return 0, errors.New("Ordinal number too high") + return 0, errors.New("ordinal number too high") } // AddressOfFunctions contains the RVAs to the "real" functions. return module.codeBase + uintptr(*(*uint32)(a2p(module.codeBase + uintptr(exports.AddressOfFunctions) + uintptr(idx)*4))), nil diff --git a/internal/wintun/memmod/memmod_windows_64.go b/internal/wintun/memmod/memmod_windows_64.go index a53851c6..b3efca92 100644 --- a/internal/wintun/memmod/memmod_windows_64.go +++ b/internal/wintun/memmod/memmod_windows_64.go @@ -29,7 +29,7 @@ func (module *Module) check4GBBoundaries(alignedImageSize uintptr) (err error) { windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - return fmt.Errorf("Error allocating memory block: %w", err) + return fmt.Errorf("error allocating memory block: %w", err) } } return diff --git a/internal/wintun/session_windows.go b/internal/wintun/session_windows.go index f023baf7..306f3b12 100644 --- a/internal/wintun/session_windows.go +++ b/internal/wintun/session_windows.go @@ -40,7 +40,7 @@ var ( ) func (wintun *Adapter) StartSession(capacity uint32) (session Session, err error) { - r0, _, e1 := syscall.Syscall(procWintunStartSession.Addr(), 2, uintptr(wintun.handle), uintptr(capacity), 0) + r0, _, e1 := syscall.SyscallN(procWintunStartSession.Addr(), uintptr(wintun.handle), uintptr(capacity)) if r0 == 0 { err = e1 } else { @@ -50,19 +50,18 @@ func (wintun *Adapter) StartSession(capacity uint32) (session Session, err error } func (session Session) End() { - syscall.Syscall(procWintunEndSession.Addr(), 1, session.handle, 0, 0) - session.handle = 0 + syscall.SyscallN(procWintunEndSession.Addr(), session.handle) } func (session Session) ReadWaitEvent() (handle windows.Handle) { - r0, _, _ := syscall.Syscall(procWintunGetReadWaitEvent.Addr(), 1, session.handle, 0, 0) + r0, _, _ := syscall.SyscallN(procWintunGetReadWaitEvent.Addr(), session.handle) handle = windows.Handle(r0) return } func (session Session) ReceivePacket() (packet []byte, err error) { var packetSize uint32 - r0, _, e1 := syscall.Syscall(procWintunReceivePacket.Addr(), 2, session.handle, uintptr(unsafe.Pointer(&packetSize)), 0) + r0, _, e1 := syscall.SyscallN(procWintunReceivePacket.Addr(), session.handle, uintptr(unsafe.Pointer(&packetSize))) if r0 == 0 { err = e1 return @@ -72,11 +71,11 @@ func (session Session) ReceivePacket() (packet []byte, err error) { } func (session Session) ReleaseReceivePacket(packet []byte) { - syscall.Syscall(procWintunReleaseReceivePacket.Addr(), 2, session.handle, uintptr(unsafe.Pointer(&packet[0])), 0) + syscall.SyscallN(procWintunReleaseReceivePacket.Addr(), session.handle, uintptr(unsafe.Pointer(&packet[0]))) } func (session Session) AllocateSendPacket(packetSize int) (packet []byte, err error) { - r0, _, e1 := syscall.Syscall(procWintunAllocateSendPacket.Addr(), 2, session.handle, uintptr(packetSize), 0) + r0, _, e1 := syscall.SyscallN(procWintunAllocateSendPacket.Addr(), session.handle, uintptr(packetSize)) if r0 == 0 { err = e1 return @@ -86,5 +85,5 @@ func (session Session) AllocateSendPacket(packetSize int) (packet []byte, err er } func (session Session) SendPacket(packet []byte) { - syscall.Syscall(procWintunSendPacket.Addr(), 2, session.handle, uintptr(unsafe.Pointer(&packet[0])), 0) + syscall.SyscallN(procWintunSendPacket.Addr(), session.handle, uintptr(unsafe.Pointer(&packet[0]))) } diff --git a/internal/wintun/wintun_windows.go b/internal/wintun/wintun_windows.go index 288d364b..8087a1e3 100644 --- a/internal/wintun/wintun_windows.go +++ b/internal/wintun/wintun_windows.go @@ -30,7 +30,7 @@ var ( ) func closeAdapter(wintun *Adapter) { - syscall.SyscallN(procWintunCloseAdapter.Addr(), 1, wintun.handle, 0, 0) + syscall.SyscallN(procWintunCloseAdapter.Addr(), wintun.handle) } // CreateAdapter creates a Wintun adapter. name is the cosmetic name of the adapter. @@ -53,7 +53,7 @@ func CreateAdapter(name string, tunnelType string, requestedGUID *windows.GUID) if err != nil { return } - r0, _, e1 := syscall.Syscall(procWintunCreateAdapter.Addr(), 3, uintptr(unsafe.Pointer(name16)), uintptr(unsafe.Pointer(tunnelType16)), uintptr(unsafe.Pointer(requestedGUID))) + r0, _, e1 := syscall.SyscallN(procWintunCreateAdapter.Addr(), uintptr(unsafe.Pointer(name16)), uintptr(unsafe.Pointer(tunnelType16)), uintptr(unsafe.Pointer(requestedGUID))) if r0 == 0 { err = e1 return @@ -70,7 +70,7 @@ func OpenAdapter(name string) (wintun *Adapter, err error) { if err != nil { return } - r0, _, e1 := syscall.Syscall(procWintunOpenAdapter.Addr(), 1, uintptr(unsafe.Pointer(name16)), 0, 0) + r0, _, e1 := syscall.SyscallN(procWintunOpenAdapter.Addr(), uintptr(unsafe.Pointer(name16))) if r0 == 0 { err = e1 return @@ -83,7 +83,7 @@ func OpenAdapter(name string) (wintun *Adapter, err error) { // Close closes a Wintun adapter. func (wintun *Adapter) Close() (err error) { runtime.SetFinalizer(wintun, nil) - r1, _, e1 := syscall.Syscall(procWintunCloseAdapter.Addr(), 1, wintun.handle, 0, 0) + r1, _, e1 := syscall.SyscallN(procWintunCloseAdapter.Addr(), wintun.handle) if r1 == 0 { err = e1 } @@ -92,7 +92,7 @@ func (wintun *Adapter) Close() (err error) { // Uninstall removes the driver from the system if no drivers are currently in use. func Uninstall() (err error) { - r1, _, e1 := syscall.Syscall(procWintunDeleteDriver.Addr(), 0, 0, 0, 0) + r1, _, e1 := syscall.SyscallN(procWintunDeleteDriver.Addr()) if r1 == 0 { err = e1 } @@ -101,7 +101,7 @@ func Uninstall() (err error) { // RunningVersion returns the version of the running Wintun driver. func RunningVersion() (version uint32, err error) { - r0, _, e1 := syscall.Syscall(procWintunGetRunningDriverVersion.Addr(), 0, 0, 0, 0) + r0, _, e1 := syscall.SyscallN(procWintunGetRunningDriverVersion.Addr()) version = uint32(r0) if version == 0 { err = e1 @@ -111,6 +111,6 @@ func RunningVersion() (version uint32, err error) { // LUID returns the LUID of the adapter. func (wintun *Adapter) LUID() (luid uint64) { - syscall.Syscall(procWintunGetAdapterLUID.Addr(), 2, uintptr(wintun.handle), uintptr(unsafe.Pointer(&luid)), 0) + syscall.SyscallN(procWintunGetAdapterLUID.Addr(), uintptr(wintun.handle), uintptr(unsafe.Pointer(&luid))) return } diff --git a/monitor_android.go b/monitor_android.go index c83440d9..5d0e9c62 100644 --- a/monitor_android.go +++ b/monitor_android.go @@ -11,7 +11,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return E.Cause(err, "list rules") } - oldVPNEnabled := m.androidVPNEnabled + oldVPNEnabled := m.androidVPNEnabled.Load() var defaultTableIndex int var vpnEnabled bool for _, rule := range ruleList { @@ -30,7 +30,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { break } } - m.androidVPNEnabled = vpnEnabled + m.androidVPNEnabled.Store(vpnEnabled) if defaultTableIndex == 0 { return ErrNoRoute @@ -56,11 +56,11 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return E.Cause(err, "find updated interface: ", link.Attrs().Name) } oldInterface := m.defaultInterface.Swap(newInterface) - if oldInterface != nil && oldInterface.Equals(*newInterface) && oldVPNEnabled == m.androidVPNEnabled { + if oldInterface != nil && oldInterface.Equals(*newInterface) && oldVPNEnabled == m.androidVPNEnabled.Load() { return nil } var flags int - if oldVPNEnabled != m.androidVPNEnabled { + if oldVPNEnabled != m.androidVPNEnabled.Load() { flags = FlagAndroidVPNUpdate } m.emit(newInterface, flags) diff --git a/monitor_shared.go b/monitor_shared.go index 8d239d29..ad48ce04 100644 --- a/monitor_shared.go +++ b/monitor_shared.go @@ -39,8 +39,8 @@ type defaultInterfaceMonitor struct { overrideAndroidVPN bool underNetworkExtension bool defaultInterface atomic.Pointer[control.Interface] - androidVPNEnabled bool - noRoute bool + androidVPNEnabled atomic.Bool + noRoute atomic.Bool networkMonitor NetworkUpdateMonitor logger logger.Logger checkUpdateTimer *time.Timer @@ -67,6 +67,8 @@ func (m *defaultInterfaceMonitor) Start() error { } func (m *defaultInterfaceMonitor) delayCheckUpdate() { + m.access.Lock() + defer m.access.Unlock() if m.checkUpdateTimer == nil { m.checkUpdateTimer = time.AfterFunc(time.Second, m.postCheckUpdate) } else { @@ -82,15 +84,15 @@ func (m *defaultInterfaceMonitor) postCheckUpdate() { } err = m.checkUpdate() if errors.Is(err, ErrNoRoute) { - if !m.noRoute { - m.noRoute = true + if !m.noRoute.Load() { + m.noRoute.Store(true) m.defaultInterface.Store(nil) m.emit(nil, 0) } } else if err != nil { m.logger.Error("check interface: ", err) } else { - m.noRoute = false + m.noRoute.Store(false) } } @@ -110,7 +112,7 @@ func (m *defaultInterfaceMonitor) OverrideAndroidVPN() bool { } func (m *defaultInterfaceMonitor) AndroidVPNEnabled() bool { - return m.androidVPNEnabled + return m.androidVPNEnabled.Load() } func (m *defaultInterfaceMonitor) RegisterCallback(callback DefaultInterfaceUpdateCallback) *list.Element[DefaultInterfaceUpdateCallback] { diff --git a/nfqueue_linux.go b/nfqueue_linux.go index baaefb54..64e73907 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -4,14 +4,12 @@ package tun import ( "context" - "errors" + "net/netip" "sync/atomic" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" "github.com/florianl/go-nfqueue/v2" "github.com/mdlayher/netlink" @@ -169,30 +167,31 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { return 0 } - var srcAddr, dstAddr M.Socksaddr + var sourceAddr, destinationAddr netip.Addr var tcpOffset int version := payload[0] >> 4 - if version == 4 { + switch version { + case 4: ipv4 := header.IPv4(payload) if !ipv4.IsValid(len(payload)) || ipv4.Protocol() != uint8(unix.IPPROTO_TCP) { h.setVerdict(packetID, nfqueue.NfAccept, 0) return 0 } - srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0) - dstAddr = M.SocksaddrFrom(ipv4.DestinationAddr(), 0) + sourceAddr = ipv4.SourceAddr() + destinationAddr = ipv4.DestinationAddr() tcpOffset = int(ipv4.HeaderLength()) - } else if version == 6 { + case 6: transportProto, transportOffset, ok := parseIPv6TransportHeader(payload) if !ok || transportProto != unix.IPPROTO_TCP { h.setVerdict(packetID, nfqueue.NfAccept, 0) return 0 } ipv6 := header.IPv6(payload) - srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0) - dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0) + sourceAddr = ipv6.SourceAddr() + destinationAddr = ipv6.DestinationAddr() tcpOffset = transportOffset - } else { + default: h.setVerdict(packetID, nfqueue.NfAccept, 0) return 0 } @@ -203,8 +202,6 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { } tcp := header.TCP(payload[tcpOffset:]) - srcAddr = M.SocksaddrFrom(srcAddr.Addr, tcp.SourcePort()) - dstAddr = M.SocksaddrFrom(dstAddr.Addr, tcp.DestinationPort()) flags := tcp.Flags() if !flags.Contains(header.TCPFlagSyn) || flags.Contains(header.TCPFlagAck) { @@ -212,18 +209,23 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { return 0 } - _, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0) + verdict := h.handler.JudgeFlow( + uint8(unix.IPPROTO_TCP), + netip.AddrPortFrom(sourceAddr, tcp.SourcePort()), + netip.AddrPortFrom(destinationAddr, tcp.DestinationPort()), + nil, + ) // Use NfRepeat for bypass/reset so the packet re-enters the chain // from the beginning, allowing mark-checking rules to save the mark // to conntrack. NfAccept is a terminal verdict in nftables — it exits // the chain immediately, skipping any rules after the queue statement. - switch { - case errors.Is(pErr, ErrBypass): + switch verdict.Action { + case ActionBypass: h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark) - case errors.Is(pErr, ErrReset): + case ActionReject: h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark) - case errors.Is(pErr, ErrDrop): + case ActionDrop: h.setVerdict(packetID, nfqueue.NfDrop, 0) default: h.setVerdict(packetID, nfqueue.NfAccept, 0) diff --git a/ping/destination.go b/ping/destination.go index 8648ecc8..e9e65291 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -9,8 +9,8 @@ import ( "sync" "time" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -20,15 +20,18 @@ import ( // Although its theoretical maximum may be 64k, I don’t yet know of any practical use case for that. For memory-usage reasons, I’m just using a 2k buffer. const maxICMPPacketSize = 2048 -var _ tun.DirectRouteDestination = (*Destination)(nil) +type PacketWriter interface { + WritePacket(packet []byte) error +} type Destination struct { conn *Conn ctx context.Context logger logger.ContextLogger destination netip.Addr - routeContext tun.DirectRouteContext + writer PacketWriter timeout time.Duration + lastActive common.TypedValue[time.Time] requestAccess sync.Mutex requests map[pingRequest]time.Time } @@ -45,9 +48,9 @@ func ConnectDestination( logger logger.ContextLogger, controlFunc control.Func, destination netip.Addr, - routeContext tun.DirectRouteContext, + writer PacketWriter, timeout time.Duration, -) (tun.DirectRouteDestination, error) { +) (*Destination, error) { var ( conn *Conn err error @@ -65,14 +68,15 @@ func ConnectDestination( return nil, err } d := &Destination{ - conn: conn, - ctx: ctx, - logger: logger, - destination: destination, - routeContext: routeContext, - timeout: timeout, - requests: make(map[pingRequest]time.Time), + conn: conn, + ctx: ctx, + logger: logger, + destination: destination, + writer: writer, + timeout: timeout, + requests: make(map[pingRequest]time.Time), } + d.lastActive.Store(time.Now()) go d.loopRead() return d, nil } @@ -80,14 +84,21 @@ func ConnectDestination( func (d *Destination) loopRead() { defer d.Close() for { - buffer := buf.NewSize(maxICMPPacketSize) - err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) + deadline := d.lastActive.Load().Add(d.timeout) + if !time.Now().Before(deadline) { + return + } + err := d.conn.SetReadDeadline(deadline) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) } + buffer := buf.NewSize(maxICMPPacketSize) err = d.conn.ReadIP(buffer) if err != nil { buffer.Release() + if E.IsTimeout(err) { + continue + } if !E.IsClosed(err) { d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } @@ -105,23 +116,29 @@ func (d *Destination) loopRead() { } icmpHdr := header.ICMPv4(ipHdr.Payload()) if d.needFilter() { - if icmpHdr.Type() != header.ICMPv4EchoReply { - continue - } - var requestExists bool - request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} - d.requestAccess.Lock() - _, loaded := d.requests[request] - if loaded { - requestExists = true - delete(d.requests, request) - } - d.requestAccess.Unlock() - if !requestExists { + switch icmpHdr.Type() { + case header.ICMPv4EchoReply: + request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} + d.requestAccess.Lock() + _, loaded := d.requests[request] + if loaded { + delete(d.requests, request) + } + d.requestAccess.Unlock() + if !loaded { + continue + } + d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) + case header.ICMPv4TimeExceeded, header.ICMPv4DstUnreachable: + if !d.rewriteICMPv4Error(ipHdr, icmpHdr) { + continue + } + default: continue } + } else { + d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } - d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } else { ipHdr := header.IPv6(buffer.Bytes()) if !ipHdr.IsValid(buffer.Len()) { @@ -152,7 +169,8 @@ func (d *Destination) loopRead() { } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } - err = d.routeContext.WritePacket(buffer.Bytes()) + d.lastActive.Store(time.Now()) + err = d.writer.WritePacket(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) } @@ -161,6 +179,7 @@ func (d *Destination) loopRead() { } func (d *Destination) WritePacket(packet *buf.Buffer) error { + d.lastActive.Store(time.Now()) if !d.destination.Is6() { ipHdr := header.IPv4(packet.Bytes()) if !ipHdr.IsValid(packet.Len()) { @@ -191,6 +210,45 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { return d.conn.WriteIP(packet) } +func (d *Destination) rewriteICMPv4Error(ipHdr header.IPv4, icmpHdr header.ICMPv4) bool { + inner := icmpHdr.Payload() + if len(inner) < header.IPv4MinimumSize { + return false + } + innerIPHdr := header.IPv4(inner) + headerLen := int(innerIPHdr.HeaderLength()) + if headerLen < header.IPv4MinimumSize || len(inner) < headerLen+header.ICMPv4MinimumSize { + return false + } + if innerIPHdr.TransportProtocol() != header.ICMPv4ProtocolNumber { + return false + } + innerICMP := header.ICMPv4(inner[headerLen:]) + if innerICMP.Type() != header.ICMPv4Echo { + return false + } + originalIdent := ^innerICMP.Ident() + request := pingRequest{ + Source: ipHdr.DestinationAddr(), + Destination: innerIPHdr.DestinationAddr(), + Identifier: originalIdent, + Sequence: innerICMP.Sequence(), + } + d.requestAccess.Lock() + _, loaded := d.requests[request] + d.requestAccess.Unlock() + if !loaded { + return false + } + innerICMP.SetIdent(originalIdent) + innerICMP.SetChecksum(header.ICMPv4Checksum(innerICMP, 0)) + innerIPHdr.SetSourceAddr(ipHdr.DestinationAddr()) + innerIPHdr.SetChecksum(^innerIPHdr.CalculateChecksum()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + d.logger.TraceContext(d.ctx, "read ICMPv4 error type ", int(icmpHdr.Type()), " from ", ipHdr.SourceAddr(), " seq ", innerICMP.Sequence()) + return true +} + func (d *Destination) needFilter() bool { return !d.conn.isLinuxUnprivileged() } diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go deleted file mode 100644 index 25fd36e5..00000000 --- a/ping/destination_gvisor.go +++ /dev/null @@ -1,129 +0,0 @@ -//go:build with_gvisor - -package ping - -import ( - "context" - "net/netip" - "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" - "github.com/sagernet/gvisor/pkg/waiter" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/logger" -) - -var _ tun.DirectRouteDestination = (*GVisorDestination)(nil) - -type GVisorDestination struct { - ctx context.Context - logger logger.ContextLogger - endpoint tcpip.Endpoint - conn *gonet.TCPConn - rewriter *SourceRewriter - timeout time.Duration -} - -func ConnectGVisor( - ctx context.Context, logger logger.ContextLogger, - sourceAddress, destinationAddress netip.Addr, - routeContext tun.DirectRouteContext, - stack *stack.Stack, - bindAddress4, bindAddress6 netip.Addr, - timeout time.Duration, -) (*GVisorDestination, error) { - var ( - bindAddress tcpip.Address - wq waiter.Queue - endpoint tcpip.Endpoint - gErr tcpip.Error - ) - if !destinationAddress.Is6() { - if !bindAddress4.IsValid() { - return nil, E.New("missing IPv4 interface address") - } - bindAddress = tun.AddressFromAddr(bindAddress4) - endpoint, gErr = stack.NewRawEndpoint(header.ICMPv4ProtocolNumber, header.IPv4ProtocolNumber, &wq, true) - } else { - if !bindAddress6.IsValid() { - return nil, E.New("missing IPv6 interface address") - } - bindAddress = tun.AddressFromAddr(bindAddress6) - endpoint, gErr = stack.NewRawEndpoint(header.ICMPv6ProtocolNumber, header.IPv6ProtocolNumber, &wq, true) - } - if gErr != nil { - return nil, gonet.TranslateNetstackError(gErr) - } - gErr = endpoint.Bind(tcpip.FullAddress{ - NIC: 1, - Addr: bindAddress, - }) - if gErr != nil { - return nil, gonet.TranslateNetstackError(gErr) - } - gErr = endpoint.Connect(tcpip.FullAddress{ - NIC: 1, - Addr: tun.AddressFromAddr(destinationAddress), - }) - if gErr != nil { - return nil, gonet.TranslateNetstackError(gErr) - } - endpoint.SocketOptions().SetHeaderIncluded(true) - rewriter := NewSourceRewriter(ctx, logger, bindAddress4, bindAddress6) - rewriter.CreateSession(tun.DirectRouteSession{Source: sourceAddress, Destination: destinationAddress}, routeContext) - destination := &GVisorDestination{ - ctx: ctx, - logger: logger, - endpoint: endpoint, - conn: gonet.NewTCPConn(&wq, endpoint), - rewriter: rewriter, - timeout: timeout, - } - go destination.loopRead() - return destination, nil -} - -func (d *GVisorDestination) loopRead() { - defer d.endpoint.Close() - for { - buffer := buf.NewSize(maxICMPPacketSize) - err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) - if err != nil { - d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) - } - n, err := d.conn.Read(buffer.FreeBytes()) - if err != nil { - buffer.Release() - if !E.IsClosed(err) { - d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) - } - return - } - buffer.Truncate(n) - _, err = d.rewriter.WriteBack(buffer.Bytes()) - if err != nil { - d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) - } - buffer.Release() - } -} - -func (d *GVisorDestination) WritePacket(packet *buf.Buffer) error { - d.rewriter.RewritePacket(packet.Bytes()) - return common.Error(d.conn.Write(packet.Bytes())) -} - -func (d *GVisorDestination) Close() error { - return d.conn.Close() -} - -func (d *GVisorDestination) IsClosed() bool { - return transport.DatagramEndpointState(d.endpoint.State()) == transport.DatagramEndpointStateClosed -} diff --git a/ping/destination_rewriter.go b/ping/destination_rewriter.go deleted file mode 100644 index a61e1556..00000000 --- a/ping/destination_rewriter.go +++ /dev/null @@ -1,79 +0,0 @@ -package ping - -import ( - "net/netip" - - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common/buf" -) - -type DestinationWriter struct { - tun.DirectRouteDestination - destination netip.Addr -} - -func NewDestinationWriter(routeDestination tun.DirectRouteDestination, destination netip.Addr) *DestinationWriter { - return &DestinationWriter{routeDestination, destination} -} - -func (w *DestinationWriter) WritePacket(packet *buf.Buffer) error { - var ipHdr header.Network - switch header.IPVersion(packet.Bytes()) { - case header.IPv4Version: - ipHdr = header.IPv4(packet.Bytes()) - case header.IPv6Version: - ipHdr = header.IPv6(packet.Bytes()) - default: - return w.DirectRouteDestination.WritePacket(packet) - } - ipHdr.SetDestinationAddr(w.destination) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - if ipHdr.TransportProtocol() == header.ICMPv6ProtocolNumber { - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - } - return w.DirectRouteDestination.WritePacket(packet) -} - -type ContextDestinationWriter struct { - tun.DirectRouteContext - destination netip.Addr -} - -func NewContextDestinationWriter(context tun.DirectRouteContext, destination netip.Addr) *ContextDestinationWriter { - return &ContextDestinationWriter{ - context, destination, - } -} - -func (w *ContextDestinationWriter) WritePacket(packet []byte) error { - var ipHdr header.Network - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - case header.IPv6Version: - ipHdr = header.IPv6(packet) - default: - return w.DirectRouteContext.WritePacket(packet) - } - ipHdr.SetSourceAddr(w.destination) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - if ipHdr.TransportProtocol() == header.ICMPv6ProtocolNumber { - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - } - return w.DirectRouteContext.WritePacket(packet) -} diff --git a/ping/destination_test.go b/ping/destination_test.go index d0a1af88..a09f1464 100644 --- a/ping/destination_test.go +++ b/ping/destination_test.go @@ -2,11 +2,16 @@ package ping_test import ( "context" + "errors" "net/netip" + "os" + "slices" "testing" "time" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/logger" "github.com/stretchr/testify/require" @@ -22,3 +27,83 @@ func TestIsClosed(t *testing.T) { destination.Close() require.True(t, destination.IsClosed()) } + +type channelWriter struct { + packets chan []byte +} + +func (w *channelWriter) WritePacket(packet []byte) error { + select { + case w.packets <- slices.Clone(packet): + default: + } + return nil +} + +type discardWriter struct{} + +func (w discardWriter) WritePacket(packet []byte) error { + return nil +} + +func buildEchoRequest(source, destination netip.Addr, identifier, sequence uint16) *buf.Buffer { + const totalLen = header.IPv4MinimumSize + header.ICMPv4MinimumSize + packet := buf.NewSize(totalLen) + ipHdr := header.IPv4(packet.Extend(totalLen)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: totalLen, + TTL: 64, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: source, + DstAddr: destination, + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetType(header.ICMPv4Echo) + icmpHdr.SetIdent(identifier) + icmpHdr.SetSequence(sequence) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + return packet +} + +// darwin unprivileged ICMP sockets and Linux raw sockets receive every +// loopback ICMP packet regardless of the flow it belongs to, so this test +// cannot run parallel to TestPing. +func TestDestinationIdleExpiry(t *testing.T) { + loopback := netip.MustParseAddr("127.0.0.1") + writer := &channelWriter{packets: make(chan []byte, 16)} + destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, writer, time.Second) + if errors.Is(err, os.ErrPermission) { + t.SkipNow() + } + require.NoError(t, err) + defer destination.Close() + + err = destination.WritePacket(buildEchoRequest(loopback, loopback, 0x1111, 1)) + require.NoError(t, err) + select { + case packet := <-writer.packets: + replyIPHdr := header.IPv4(packet) + replyICMPHdr := header.ICMPv4(replyIPHdr.Payload()) + require.Equal(t, header.ICMPv4EchoReply, replyICMPHdr.Type()) + require.Equal(t, uint16(0x1111), replyICMPHdr.Ident()) + case <-time.After(3 * time.Second): + t.Fatal("no echo reply received") + } + + noise, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, discardWriter{}, 30*time.Second) + require.NoError(t, err) + defer noise.Close() + + deadline := time.Now().Add(5 * time.Second) + var sequence uint16 + for time.Now().Before(deadline) { + sequence++ + _ = noise.WritePacket(buildEchoRequest(loopback, loopback, 0x2222, sequence)) + if destination.IsClosed() { + return + } + time.Sleep(150 * time.Millisecond) + } + t.Fatal("flow not closed after idle timeout despite unrelated ICMP traffic") +} diff --git a/ping/filter_linux.go b/ping/filter_linux.go new file mode 100644 index 00000000..2a1d66da --- /dev/null +++ b/ping/filter_linux.go @@ -0,0 +1,99 @@ +package ping + +import ( + "sync" + "syscall" + + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + + "golang.org/x/net/bpf" + "golang.org/x/sys/unix" +) + +type identFilterState struct { + access sync.Mutex + attached bool + disabled bool + identifier uint16 +} + +// The kernel clones every matching-protocol packet into every unconnected raw +// ICMP socket, so without a socket filter each flow receives and discards all +// other flows' traffic. +func (c *Conn) updateIdentFilter(wireIdentifier uint16) { + if !c.privileged { + return + } + syscallConn, isSyscallConn := common.Cast[syscall.Conn](c.conn) + if !isSyscallConn { + return + } + state := &c.identFilter + state.access.Lock() + defer state.access.Unlock() + if state.disabled { + return + } + if state.attached { + if state.identifier == wireIdentifier { + return + } + state.attached = false + state.disabled = true + _ = control.Conn(syscallConn, func(fd uintptr) error { + return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0) + }) + return + } + rawInstructions, err := identFilterProgram(c.destination.Is6(), wireIdentifier) + if err != nil { + state.disabled = true + return + } + filter := make([]unix.SockFilter, len(rawInstructions)) + for i, instruction := range rawInstructions { + filter[i] = unix.SockFilter{Code: instruction.Op, Jt: instruction.Jt, Jf: instruction.Jf, K: instruction.K} + } + program := unix.SockFprog{Len: uint16(len(filter)), Filter: &filter[0]} + err = control.Conn(syscallConn, func(fd uintptr) error { + return unix.SetsockoptSockFprog(int(fd), unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, &program) + }) + if err != nil { + state.disabled = true + return + } + state.attached = true + state.identifier = wireIdentifier +} + +func identFilterProgram(is6 bool, wireIdentifier uint16) ([]bpf.RawInstruction, error) { + if !is6 { + // Raw ICMPv4 sockets deliver the full packet including the IP header. + return bpf.Assemble([]bpf.Instruction{ + bpf.LoadMemShift{Off: 0}, + bpf.LoadIndirect{Off: 0, Size: 1}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv4EchoReply), SkipTrue: 3}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv4DstUnreachable), SkipTrue: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv4TimeExceeded), SkipTrue: 3}, + bpf.RetConstant{Val: 0}, + bpf.LoadIndirect{Off: 4, Size: 2}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(wireIdentifier), SkipFalse: 1}, + bpf.RetConstant{Val: 0xffffffff}, + bpf.RetConstant{Val: 0}, + }) + } + // Raw ICMPv6 sockets deliver the ICMPv6 message without the IP header. + return bpf.Assemble([]bpf.Instruction{ + bpf.LoadAbsolute{Off: 0, Size: 1}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv6EchoReply), SkipTrue: 3}, + bpf.JumpIf{Cond: bpf.JumpGreaterThan, Val: uint32(header.ICMPv6ParamProblem), SkipTrue: 1}, + bpf.RetConstant{Val: 0xffffffff}, + bpf.RetConstant{Val: 0}, + bpf.LoadAbsolute{Off: 4, Size: 2}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(wireIdentifier), SkipFalse: 1}, + bpf.RetConstant{Val: 0xffffffff}, + bpf.RetConstant{Val: 0}, + }) +} diff --git a/ping/filter_other.go b/ping/filter_other.go new file mode 100644 index 00000000..34c2137d --- /dev/null +++ b/ping/filter_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package ping + +type identFilterState struct{} + +func (c *Conn) updateIdentFilter(wireIdentifier uint16) { +} diff --git a/ping/ping.go b/ping/ping.go index 248987c2..dbb4f929 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" @@ -24,9 +24,11 @@ type Conn struct { ctx context.Context privileged bool conn net.Conn + controlConn net.Conn destination netip.Addr source common.TypedValue[netip.Addr] closed atomic.Bool + identFilter identFilterState readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) } @@ -53,6 +55,7 @@ func (c *Conn) connect(controlFunc control.Func, idleTimeout time.Duration) (err return err } if ipConn, isIPConn := common.Cast[*net.IPConn](c.conn); isIPConn { + c.controlConn = ipConn c.readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var ipAddr *net.IPAddr n, oobn, _, ipAddr, err = ipConn.ReadMsgIP(b, oob) @@ -62,6 +65,7 @@ func (c *Conn) connect(controlFunc control.Func, idleTimeout time.Duration) (err return } } else if udpConn, isUDPConn := common.Cast[*net.UDPConn](c.conn); isUDPConn { + c.controlConn = udpConn c.readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var addrPort netip.AddrPort n, oobn, _, addrPort, err = udpConn.ReadMsgUDPAddrPort(b, oob) @@ -158,9 +162,19 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { }) } } else { - _, err := buffer.ReadOnceFrom(c.conn) - if err != nil { - return err + if runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "windows" { + // An unconnected SOCK_RAW IPv4 socket delivers the full packet including the IP + // header via ReadMsgIP, whereas ReadFrom strips it. + n, _, _, err := c.readMsg(buffer.FreeBytes(), nil) + if err != nil { + return err + } + buffer.Truncate(n) + } else { + _, err := buffer.ReadOnceFrom(c.conn) + if err != nil { + return err + } } if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) @@ -177,10 +191,12 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetDestinationAddr(c.source.Load()) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) - if !c.isLinuxUnprivileged() { - icmpHdr.SetIdent(^icmpHdr.Ident()) + if icmpHdr.Type() == header.ICMPv4EchoReply { + if !c.isLinuxUnprivileged() { + icmpHdr.SetIdent(^icmpHdr.Ident()) + } + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } else { ipHdr := header.IPv6(buffer.Bytes()) if !ipHdr.IsValid(buffer.Len()) { @@ -202,27 +218,41 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } func (c *Conn) ReadICMP(buffer *buf.Buffer) error { + if !c.isLinuxUnprivileged() && !c.destination.Is6() { + if runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "windows" { + // An unconnected SOCK_RAW IPv4 socket delivers the full packet including the IP + // header via ReadMsgIP, whereas ReadFrom strips it. + n, _, _, err := c.readMsg(buffer.FreeBytes(), nil) + if err != nil { + return err + } + buffer.Truncate(n) + } else { + _, err := buffer.ReadOnceFrom(c.conn) + if err != nil { + return err + } + } + ipHdr := header.IPv4(buffer.Bytes()) + buffer.Advance(int(ipHdr.HeaderLength())) + + icmpHdr := header.ICMPv4(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + return nil + } _, err := buffer.ReadOnceFrom(c.conn) if err != nil { return err } - if !c.isLinuxUnprivileged() { - if !c.destination.Is6() { - ipHdr := header.IPv4(buffer.Bytes()) - buffer.Advance(int(ipHdr.HeaderLength())) - - icmpHdr := header.ICMPv4(buffer.Bytes()) - icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) - } else { - icmpHdr := header.ICMPv6(buffer.Bytes()) - icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: c.destination.AsSlice(), - Dst: c.source.Load().AsSlice(), - })) - } + if c.destination.Is6() && !c.isLinuxUnprivileged() { + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: c.destination.AsSlice(), + Dst: c.source.Load().AsSlice(), + })) } return nil } @@ -232,15 +262,24 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) if !c.isLinuxUnprivileged() { + err := ipv4.NewConn(c.controlConn).SetTTL(int(ipHdr.TTL())) + if err != nil { + return err + } icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + c.updateIdentFilter(icmpHdr.Ident()) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) if !c.isLinuxUnprivileged() { + err := ipv6.NewConn(c.controlConn).SetHopLimit(int(ipHdr.HopLimit())) + if err != nil { + return err + } icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ @@ -248,6 +287,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { Src: ipHdr.SourceAddressSlice(), Dst: ipHdr.DestinationAddressSlice(), })) + c.updateIdentFilter(icmpHdr.Ident()) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) return common.Error(c.conn.Write(ipHdr.Payload())) diff --git a/ping/ping_test.go b/ping/ping_test.go index 5a04be17..74f3df8c 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -9,7 +9,7 @@ import ( "time" "github.com/sagernet/gvisor/pkg/rand" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" @@ -30,6 +30,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv4ReadIP(t, false, addr4) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv4WriteIP(t, false, addr4) + }) }) t.Run("privileged", func(t *testing.T) { if runtime.GOOS != "windows" && os.Getuid() != 0 { @@ -41,6 +44,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv4ReadIP(t, true, addr4) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv4WriteIP(t, true, addr4) + }) }) }) // const addr6 = "2606:4700:4700::1001" @@ -56,6 +62,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv6ReadIP(t, false, addr6) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv6WriteIP(t, false, addr6) + }) }) t.Run("privileged", func(t *testing.T) { if runtime.GOOS != "windows" && os.Getuid() != 0 { @@ -67,6 +76,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv6ReadIP(t, true, addr6) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv6WriteIP(t, true, addr6) + }) }) }) } @@ -196,3 +208,100 @@ func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type()) require.Equal(t, request.Ident(), icmpHdr.Ident()) } + +// testPingIPv4WriteIP exercises the WriteIP send path, which is what the real +// TUN flow uses (Destination.WritePacket -> Conn.WriteIP). Unlike the ReadIP/ +// ReadICMP tests, which send via WriteICMP, this path runs the SetTTL call that +// regressed in ebb52fb: on privileged Linux / unprivileged macOS the socket is +// wrapped in a BindPacketConn that does not expose SyscallConn, so +// ipv4.NewConn(c.conn).SetTTL returned "invalid connection" and the echo +// request was never sent. +func testPingIPv4WriteIP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + defer conn.Close() + + ident := uint16(rand.Uint32()) + const totalLen = header.IPv4MinimumSize + header.ICMPv4MinimumSize + packet := buf.NewSize(totalLen) + ipHdr := header.IPv4(packet.Extend(totalLen)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: totalLen, + TTL: 64, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: netip.MustParseAddr("127.0.0.1"), + DstAddr: netip.MustParseAddr(addr), + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetType(header.ICMPv4Echo) + icmpHdr.SetIdent(ident) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + + conn.SetLocalAddr(netip.MustParseAddr("127.0.0.1")) + err = conn.WriteIP(packet) + require.NoError(t, err, "WriteIP must send the echo request") + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + response := buf.NewPacket() + defer response.Release() + err = conn.ReadIP(response) + require.NoError(t, err) + respIP := header.IPv4(response.Bytes()) + require.NotZero(t, respIP.TTL()) + respICMP := header.ICMPv4(respIP.Payload()) + require.Equal(t, header.ICMPv4EchoReply, respICMP.Type()) + require.Equal(t, ident, respICMP.Ident()) +} + +func testPingIPv6WriteIP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + defer conn.Close() + + ident := uint16(rand.Uint32()) + const payloadLen = header.ICMPv6MinimumSize + packet := buf.NewSize(header.IPv6MinimumSize + payloadLen) + ipHdr := header.IPv6(packet.Extend(header.IPv6MinimumSize + payloadLen)) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: payloadLen, + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: 64, + SrcAddr: netip.MustParseAddr("::1"), + DstAddr: netip.MustParseAddr(addr), + }) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetType(header.ICMPv6EchoRequest) + icmpHdr.SetIdent(ident) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), + })) + + conn.SetLocalAddr(netip.MustParseAddr("::1")) + err = conn.WriteIP(packet) + require.NoError(t, err, "WriteIP must send the echo request") + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + response := buf.NewPacket() + defer response.Release() + err = conn.ReadIP(response) + require.NoError(t, err) + if runtime.GOOS == "darwin" { + response.Reset() + err = conn.ReadIP(response) + require.NoError(t, err) + } + respIP := header.IPv6(response.Bytes()) + require.NotZero(t, respIP.HopLimit()) + respICMP := header.ICMPv6(respIP.Payload()) + require.Equal(t, header.ICMPv6EchoReply, respICMP.Type()) + require.Equal(t, ident, respICMP.Ident()) +} diff --git a/ping/port.go b/ping/port.go new file mode 100644 index 00000000..e0cc2a1d --- /dev/null +++ b/ping/port.go @@ -0,0 +1,194 @@ +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...) +} diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index 3742cc83..f709684a 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 1eec10a5..91282390 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -87,24 +87,33 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) return nil, err } - if runtime.GOOS == "darwin" && !privileged { - // When running in NetworkExtension on macOS, write to connected socket results in EPIPE. + useUnconnected := (runtime.GOOS == "darwin" && !privileged) || + ((runtime.GOOS == "linux" || runtime.GOOS == "android") && privileged) + if useUnconnected { + // A connected ICMP socket only receives messages whose source is the connected peer, + // so the Time Exceeded replies that transit routers send for traceroute never reach it. + // Additionally, on macOS NetworkExtension, writing to a connected socket returns EPIPE. var packetConn net.PacketConn packetConn, err = net.FilePacketConn(file) if err != nil { return nil, err } - return bufio.NewBindPacketConn(packetConn, M.SocksaddrFrom(destination, 0).UDPAddr()), nil - } else { - err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) - if err != nil { - return nil, err - } - var conn net.Conn - conn, err = net.FileConn(file) - if err != nil { - return nil, err + var writeTarget net.Addr + if privileged { + writeTarget = M.SocksaddrFrom(destination, 0).IPAddr() + } else { + writeTarget = M.SocksaddrFrom(destination, 0).UDPAddr() } - return conn, nil + return bufio.NewBindPacketConn(packetConn, writeTarget), nil + } + err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) + if err != nil { + return nil, err + } + var conn net.Conn + conn, err = net.FileConn(file) + if err != nil { + return nil, err } + return conn, nil } diff --git a/ping/socket_windows.go b/ping/socket_windows.go index daafd18a..332a2513 100644 --- a/ping/socket_windows.go +++ b/ping/socket_windows.go @@ -1,30 +1,29 @@ package ping import ( + "context" "net" "net/netip" "syscall" + "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" + M "github.com/sagernet/sing/common/metadata" "golang.org/x/sys/windows" ) func connect(privileged bool, controlFunc control.Func, destination netip.Addr) (net.Conn, error) { - var dialer net.Dialer - dialer.Control = controlFunc + var listenConfig net.ListenConfig + listenConfig.Control = controlFunc if destination.Is6() { - dialer.Control = control.Append(dialer.Control, func(network, address string, conn syscall.RawConn) error { + listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_HOPLIMIT, 1) if err != nil { return err } - err = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_RECVTCLASS, 1) - if err != nil { - return err - } - return nil + return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_RECVTCLASS, 1) }) }) } @@ -34,5 +33,11 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) } else { network = "ip6:ipv6-icmp" } - return dialer.Dial(network, destination.String()) + // A connected raw socket only receives messages from the connected peer, so transit routers' + // Time Exceeded replies needed by traceroute never arrive. + packetConn, err := listenConfig.ListenPacket(context.Background(), network, "") + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(packetConn, M.SocksaddrFrom(destination, 0).IPAddr()), nil } diff --git a/ping/source_rewriter.go b/ping/source_rewriter.go deleted file mode 100644 index 480c6a78..00000000 --- a/ping/source_rewriter.go +++ /dev/null @@ -1,150 +0,0 @@ -package ping - -import ( - "context" - "net/netip" - "sync" - - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common/logger" -) - -type SourceRewriter struct { - ctx context.Context - logger logger.ContextLogger - access sync.RWMutex - sessions map[tun.DirectRouteSession]tun.DirectRouteContext - sourceAddress map[uint16]netip.Addr - inet4Address netip.Addr - inet6Address netip.Addr -} - -func NewSourceRewriter(ctx context.Context, logger logger.ContextLogger, inet4Address netip.Addr, inet6Address netip.Addr) *SourceRewriter { - return &SourceRewriter{ - ctx: ctx, - logger: logger, - sessions: make(map[tun.DirectRouteSession]tun.DirectRouteContext), - sourceAddress: make(map[uint16]netip.Addr), - inet4Address: inet4Address, - inet6Address: inet6Address, - } -} - -func (m *SourceRewriter) CreateSession(session tun.DirectRouteSession, context tun.DirectRouteContext) { - m.access.Lock() - m.sessions[session] = context - m.access.Unlock() -} - -func (m *SourceRewriter) DeleteSession(session tun.DirectRouteSession) { - m.access.Lock() - delete(m.sessions, session) - m.access.Unlock() -} - -func (m *SourceRewriter) RewritePacket(packet []byte) { - var ipHdr header.Network - var bindAddr netip.Addr - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - bindAddr = m.inet4Address - case header.IPv6Version: - ipHdr = header.IPv6(packet) - bindAddr = m.inet6Address - default: - return - } - sourceAddr := ipHdr.SourceAddr() - ipHdr.SetSourceAddr(bindAddr) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(ipHdr.Payload()) - m.access.Lock() - m.sourceAddress[icmpHdr.Ident()] = sourceAddr - m.access.Unlock() - m.logger.TraceContext(m.ctx, "write ICMPv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - m.access.Lock() - m.sourceAddress[icmpHdr.Ident()] = sourceAddr - m.access.Unlock() - m.logger.TraceContext(m.ctx, "write ICMPv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - } -} - -func (m *SourceRewriter) WriteBack(packet []byte) (bool, error) { - var ipHdr header.Network - var routeSession tun.DirectRouteSession - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - routeSession.Destination = ipHdr.SourceAddr() - case header.IPv6Version: - ipHdr = header.IPv6(packet) - routeSession.Destination = ipHdr.SourceAddr() - default: - return false, nil - } - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(ipHdr.Payload()) - m.access.Lock() - ident := icmpHdr.Ident() - source, loaded := m.sourceAddress[ident] - if !loaded { - m.access.Unlock() - return false, nil - } - delete(m.sourceAddress, icmpHdr.Ident()) - m.access.Unlock() - routeSession.Source = source - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(ipHdr.Payload()) - m.access.Lock() - ident := icmpHdr.Ident() - source, loaded := m.sourceAddress[ident] - if !loaded { - m.access.Unlock() - return false, nil - } - delete(m.sourceAddress, icmpHdr.Ident()) - m.access.Unlock() - routeSession.Source = source - default: - return false, nil - } - m.access.RLock() - context, loaded := m.sessions[routeSession] - m.access.RUnlock() - if !loaded { - return false, nil - } - ipHdr.SetDestinationAddr(routeSession.Source) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(ipHdr.Payload()) - m.logger.TraceContext(m.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - m.logger.TraceContext(m.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - } - return true, context.WritePacket(packet) -} diff --git a/redirect_linux.go b/redirect_linux.go index e9c892c8..c679e5f0 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -44,6 +44,8 @@ type autoRedirect struct { nfqueueEnabled bool redirectRouteTableIndex int redirectInterfaces []control.Interface + dockerFirewallMonitor *nftables.Monitor + dockerFirewallDone chan struct{} } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -135,22 +137,24 @@ func (r *autoRedirect) Start() error { r.redirectServer = server } if r.useNFTables { - var handler *nfqueueHandler - handler, err = newNFQueueHandler(nfqueueOptions{ - Context: r.ctx, - Handler: r.handler, - Logger: r.logger, - Queue: r.effectiveNFQueue(), - OutputMark: r.effectiveOutputMark(), - ResetMark: r.effectiveResetMark(), - }) - if err != nil { - r.logger.Warn("nfqueue not available, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) - } else if err = handler.Start(); err != nil { - r.logger.Warn("nfqueue start failed, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) - } else { - r.nfqueueHandler = handler - r.nfqueueEnabled = true + if r.handler != nil { + var handler *nfqueueHandler + handler, err = newNFQueueHandler(nfqueueOptions{ + Context: r.ctx, + Handler: r.handler, + Logger: r.logger, + Queue: r.effectiveNFQueue(), + OutputMark: r.effectiveOutputMark(), + ResetMark: r.effectiveResetMark(), + }) + if err != nil { + r.logger.Warn("nfqueue not available, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) + } else if err = handler.Start(); err != nil { + r.logger.Warn("nfqueue start failed, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) + } else { + r.nfqueueHandler = handler + r.nfqueueEnabled = true + } } r.cleanupNFTables() err = r.setupNFTables() diff --git a/redirect_nftables.go b/redirect_nftables.go index 266bbe91..a2940871 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -283,12 +283,10 @@ func (r *autoRedirect) setupNFTables() error { if err != nil { return E.Cause(err, "configure openwrt firewall4") } - err = nft.Flush() if err != nil { return E.Cause(err, "flush nftables") } - r.networkListener = r.networkMonitor.RegisterCallback(func() { err = r.nftablesUpdateLocalAddressSet() if err != nil { @@ -361,6 +359,7 @@ func (r *autoRedirect) cleanupNFTables() { if r.networkListener != nil { r.networkMonitor.UnregisterCallback(r.networkListener) } + r.stopDockerFirewallMonitor() nft, err := nftables.New() if err != nil { return @@ -372,6 +371,10 @@ func (r *autoRedirect) cleanupNFTables() { _ = r.configureOpenWRTFirewall4(nft, true) _ = nft.Flush() _ = nft.CloseLasting() + err = r.configureDockerFirewall(true) + if err != nil && r.logger != nil { + r.logger.Warn("cleanup docker firewall: ", err) + } } func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *nftables.Table) error { diff --git a/redirect_nftables_docker.go b/redirect_nftables_docker.go new file mode 100644 index 00000000..0276e2e8 --- /dev/null +++ b/redirect_nftables_docker.go @@ -0,0 +1,239 @@ +//go:build linux + +package tun + +import ( + "bytes" + "slices" + "strings" + + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/expr" + "github.com/sagernet/nftables/userdata" + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + nftablesDockerFilterTable = "filter" + nftablesDockerUserChain = "DOCKER-USER" +) + +func (r *autoRedirect) startDockerFirewallMonitor() { + if r.dockerFirewallMonitor != nil { + return + } + doneCh := make(chan struct{}) + r.dockerFirewallDone = doneCh + monitor := nftables.NewMonitor( + nftables.WithMonitorAction(nftables.MonitorActionAny), + nftables.WithMonitorObject(nftables.MonitorObjectRuleset), + nftables.WithMonitorEventBuffer(16), + ) + nft, err := nftables.New() + if err != nil { + if r.logger != nil { + r.logger.Warn("create nftables monitor connection: ", err) + } + close(doneCh) + r.dockerFirewallDone = nil + return + } + events, err := nft.AddGenerationalMonitor(monitor) + _ = nft.CloseLasting() + if err != nil { + if r.logger != nil { + r.logger.Warn("start nftables monitor: ", err) + } + close(doneCh) + r.dockerFirewallDone = nil + return + } + r.dockerFirewallMonitor = monitor + go r.loopDockerFirewallMonitor(events, doneCh) +} + +func (r *autoRedirect) stopDockerFirewallMonitor() { + if r.dockerFirewallMonitor == nil { + return + } + _ = r.dockerFirewallMonitor.Close() + <-r.dockerFirewallDone + r.dockerFirewallMonitor = nil + r.dockerFirewallDone = nil +} + +func (r *autoRedirect) loopDockerFirewallMonitor(events <-chan *nftables.MonitorEvents, doneCh chan<- struct{}) { + defer close(doneCh) + for monitorEvents := range events { + if monitorEvents != nil && monitorEvents.GeneratedBy != nil && monitorEvents.GeneratedBy.Error != nil { + if r.logger != nil { + r.logger.Warn("nftables monitor closed: ", monitorEvents.GeneratedBy.Error) + } + return + } + if !nftablesDockerFirewallEventsRelevant(monitorEvents) { + continue + } + err := r.configureDockerFirewall(false) + if err != nil && r.logger != nil { + r.logger.Warn("update docker firewall: ", err) + } + } +} + +func (r *autoRedirect) configureDockerFirewall(cleanup bool) error { + nft, err := nftables.New() + if err != nil { + return E.Cause(err, "create nftables connection") + } + defer nft.CloseLasting() + + err = r.configureDockerFirewallWithConn(nft, cleanup) + if err != nil { + return err + } + return nft.Flush() +} + +func (r *autoRedirect) configureDockerFirewallWithConn(nft *nftables.Conn, cleanup bool) error { + var err error + if r.enableIPv4 { + err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv4, cleanup)) + } + if r.enableIPv6 { + err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv6, cleanup)) + } + return err +} + +func (r *autoRedirect) configureDockerFirewallForFamily(nft *nftables.Conn, family nftables.TableFamily, cleanup bool) error { + table, chain, loaded, err := nftablesLoadDockerUserChain(nft, family) + if err != nil || !loaded { + return err + } + err = r.configureDockerFirewallRules(nft, table, chain, cleanup) + return err +} + +func (r *autoRedirect) configureDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, cleanup bool) error { + rules, err := nft.GetRules(table, chain) + if err != nil { + return E.Cause(err, "list docker user rules") + } + if cleanup { + return r.cleanupDockerFirewallRules(nft, rules) + } + return r.reconcileDockerFirewallRules(nft, table, chain, rules) +} + +func nftablesLoadDockerUserChain(nft *nftables.Conn, family nftables.TableFamily) (*nftables.Table, *nftables.Chain, bool, error) { + table, err := nft.ListTableOfFamily(nftablesDockerFilterTable, family) + if err != nil { + return nil, nil, false, nil + } + chain, err := nft.ListChain(table, nftablesDockerUserChain) + if err != nil { + return nil, nil, false, nil + } + return table, chain, true, nil +} + +func nftablesDockerFirewallEventsRelevant(events *nftables.MonitorEvents) bool { + if events == nil { + return false + } + return slices.ContainsFunc(events.Changes, nftablesDockerFirewallEventRelevant) +} + +func nftablesDockerFirewallEventRelevant(event *nftables.MonitorEvent) bool { + if event == nil || event.Error != nil { + return false + } + switch data := event.Data.(type) { + case *nftables.Table: + return nftablesIsDockerFirewallTable(data) + case *nftables.Chain: + return data.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table) + case *nftables.Rule: + return data.Chain != nil && data.Chain.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table) + default: + return false + } +} + +func nftablesIsDockerFirewallTable(table *nftables.Table) bool { + return table != nil && + table.Name == nftablesDockerFilterTable && + (table.Family == nftables.TableFamilyIPv4 || table.Family == nftables.TableFamilyIPv6) +} + +func (r *autoRedirect) cleanupDockerFirewallRules(nft *nftables.Conn, rules []*nftables.Rule) error { + var deleteErr error + for _, rule := range rules { + if r.nftablesIsDockerCompatibilityRule(rule) { + deleteErr = E.Errors(deleteErr, nft.DelRule(rule)) + } + } + return deleteErr +} + +func (r *autoRedirect) reconcileDockerFirewallRules(nft *nftables.Conn, _ *nftables.Table, _ *nftables.Chain, rules []*nftables.Rule) error { + return r.cleanupDockerFirewallRules(nft, rules) +} + +func nftablesDockerCompatibilityRule(table *nftables.Table, chain *nftables.Chain, ifName string, ifNameKey expr.MetaKey, comment string) *nftables.Rule { + return &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: ifNameKey, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(ifName), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictAccept, + }, + }, + UserData: userdata.AppendString(nil, userdata.TypeComment, comment), + } +} + +func nftablesDockerCompatibilityRuleMatches(rule *nftables.Rule, ifName string, ifNameKey expr.MetaKey, comment string) bool { + ruleComment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment) + if !loaded || ruleComment != comment || len(rule.Exprs) != 4 { + return false + } + meta, loaded := rule.Exprs[0].(*expr.Meta) + if !loaded || meta.Key != ifNameKey || meta.Register != 1 { + return false + } + cmp, loaded := rule.Exprs[1].(*expr.Cmp) + if !loaded || cmp.Op != expr.CmpOpEq || cmp.Register != 1 || !bytes.Equal(cmp.Data, nftablesIfname(ifName)) { + return false + } + _, loaded = rule.Exprs[2].(*expr.Counter) + if !loaded { + return false + } + verdict, loaded := rule.Exprs[3].(*expr.Verdict) + return loaded && verdict.Kind == expr.VerdictAccept +} + +func (r *autoRedirect) nftablesIsDockerCompatibilityRule(rule *nftables.Rule) bool { + comment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment) + return loaded && strings.HasPrefix(comment, r.nftablesDockerCompatibilityCommentPrefix()) +} + +func (r *autoRedirect) nftablesDockerCompatibilityComment(direction string) string { + return r.nftablesDockerCompatibilityCommentPrefix() + direction +} + +func (r *autoRedirect) nftablesDockerCompatibilityCommentPrefix() string { + return "!" + r.tableName + ": Docker compatibility " +} diff --git a/redirect_nftables_docker_test.go b/redirect_nftables_docker_test.go new file mode 100644 index 00000000..c4d1cf9f --- /dev/null +++ b/redirect_nftables_docker_test.go @@ -0,0 +1,57 @@ +//go:build linux + +package tun + +import ( + "reflect" + "testing" + + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/expr" +) + +func TestDockerFirewallReconcileDoesNotInsertAcceptRules(t *testing.T) { + r := &autoRedirect{ + tunOptions: &Options{Name: "tun0"}, + tableName: "sing-tun", + } + nft, err := nftables.New() + if err != nil { + t.Fatal(err) + } + table := &nftables.Table{Name: nftablesDockerFilterTable, Family: nftables.TableFamilyIPv4} + chain := &nftables.Chain{Name: nftablesDockerUserChain, Table: table} + + if err := r.reconcileDockerFirewallRules(nft, table, chain, nil); err != nil { + t.Fatal(err) + } + if messages := nftablesConnMessageCount(nft); messages != 0 { + t.Fatalf("reconcile queued %d netlink messages; want 0", messages) + } +} + +func TestDockerFirewallReconcileCleansExistingCompatibilityRules(t *testing.T) { + r := &autoRedirect{ + tunOptions: &Options{Name: "tun0"}, + tableName: "sing-tun", + } + nft, err := nftables.New() + if err != nil { + t.Fatal(err) + } + table := &nftables.Table{Name: nftablesDockerFilterTable, Family: nftables.TableFamilyIPv4} + chain := &nftables.Chain{Name: nftablesDockerUserChain, Table: table} + rule := nftablesDockerCompatibilityRule(table, chain, "tun0", expr.MetaKeyOIFNAME, r.nftablesDockerCompatibilityComment("output to tun")) + rule.Handle = 1 + + if err := r.reconcileDockerFirewallRules(nft, table, chain, []*nftables.Rule{rule}); err != nil { + t.Fatal(err) + } + if messages := nftablesConnMessageCount(nft); messages != 1 { + t.Fatalf("reconcile queued %d netlink messages; want 1 cleanup delete", messages) + } +} + +func nftablesConnMessageCount(nft *nftables.Conn) int { + return reflect.ValueOf(nft).Elem().FieldByName("messages").Len() +} diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index dddd9c66..1ef5c19b 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -3,6 +3,7 @@ package tun import ( + "net" "net/netip" _ "unsafe" @@ -376,6 +377,149 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft }) } } + if len(r.tunOptions.IncludeMACAddress) > 0 { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFTYPE, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint16(unix.ARPHRD_ETHER), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + if len(r.tunOptions.IncludeMACAddress) > 1 { + includeMACSet := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeEtherAddr, + } + err := nft.AddSet(includeMACSet, common.Map(r.tunOptions.IncludeMACAddress, func(it net.HardwareAddr) nftables.SetElement { + return nftables.SetElement{ + Key: []byte(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: includeMACSet.ID, + SetName: includeMACSet.Name, + Invert: true, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: []byte(r.tunOptions.IncludeMACAddress[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } + if len(r.tunOptions.ExcludeMACAddress) > 0 { + if len(r.tunOptions.ExcludeMACAddress) > 1 { + excludeMACSet := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeEtherAddr, + } + err := nft.AddSet(excludeMACSet, common.Map(r.tunOptions.ExcludeMACAddress, func(it net.HardwareAddr) nftables.SetElement { + return nftables.SetElement{ + Key: []byte(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: excludeMACSet.ID, + SetName: excludeMACSet.Name, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte(r.tunOptions.ExcludeMACAddress[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } } else { if len(r.tunOptions.IncludeUID) > 0 { if len(r.tunOptions.IncludeUID) > 1 || r.tunOptions.IncludeUID[0].Start != r.tunOptions.IncludeUID[0].End { @@ -531,7 +675,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteExcludeAddress.ID, inet6RouteExcludeAddress.Name, nftables.TableFamilyIPv6, false) } - if !r.tunOptions.EXP_DisableDNSHijack && ((chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT) || + if r.tunOptions.DNSModeOrDefault() == DNSModeHijack && ((chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT) || (r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeNAT)) { if r.enableIPv4 { err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv4, 5, "inet4_local_address_set") @@ -853,23 +997,19 @@ func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( if err != nil { return E.Cause(err, "add dns protocol set") } - dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { - return it.Is4() == (family == nftables.TableFamilyIPv4) - }) - if !dnsServer.IsValid() { - if family == nftables.TableFamilyIPv4 { - if HasNextAddress(r.tunOptions.Inet4Address[0], 1) { - dnsServer = r.tunOptions.Inet4Address[0].Addr().Next() - } - } else { - if HasNextAddress(r.tunOptions.Inet6Address[0], 1) { - dnsServer = r.tunOptions.Inet6Address[0].Addr().Next() - } - } + var dnsServers []netip.Addr + if family == nftables.TableFamilyIPv4 { + dnsServers, err = r.tunOptions.Inet4DNSAddress() + } else { + dnsServers, err = r.tunOptions.Inet6DNSAddress() + } + if err != nil { + return err } - if !dnsServer.IsValid() { + if len(dnsServers) == 0 { return nil } + dnsServer := dnsServers[0] exprs := []expr.Any{ &expr.Meta{ Key: expr.MetaKeyNFPROTO, diff --git a/route_direct.go b/route_direct.go deleted file mode 100644 index 444eb5e0..00000000 --- a/route_direct.go +++ /dev/null @@ -1,61 +0,0 @@ -package tun - -import ( - "net/netip" - "time" - - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/contrab/freelru" - "github.com/sagernet/sing/contrab/maphash" -) - -type DirectRouteDestination interface { - WritePacket(packet *buf.Buffer) error - Close() error - IsClosed() bool -} - -type DirectRouteSession struct { - // IPVersion uint8 - // Network uint8 - Source netip.Addr - Destination netip.Addr -} - -type DirectRouteMapping struct { - mapping freelru.Cache[DirectRouteSession, DirectRouteDestination] - timeout time.Duration -} - -func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { - mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) - mapping.SetHealthCheck(func(session DirectRouteSession, action DirectRouteDestination) bool { - if action != nil { - return !action.IsClosed() - } - return true - }) - mapping.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { - if action != nil { - action.Close() - } - }) - mapping.SetLifetime(timeout) - return &DirectRouteMapping{mapping, timeout} -} - -func (m *DirectRouteMapping) Lookup(session DirectRouteSession, constructor func(timeout time.Duration) (DirectRouteDestination, error)) (DirectRouteDestination, error) { - var ( - created DirectRouteDestination - err error - ) - action, _, ok := m.mapping.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { - created, err = constructor(m.timeout) - return created, err == nil - }) - if !ok { - return nil, err - } - return action, nil -} diff --git a/stack.go b/stack.go index a6f6043a..eaf24058 100644 --- a/stack.go +++ b/stack.go @@ -12,12 +12,6 @@ import ( "github.com/sagernet/sing/common/logger" ) -var ( - ErrDrop = E.New("drop by rule") - ErrReset = E.New("reset by rule") - ErrBypass = E.New("bypass by rule") -) - type Stack interface { Start() error Close() error @@ -68,7 +62,7 @@ func NewStack( func HasNextAddress(prefix netip.Prefix, count int) bool { checkAddr := prefix.Addr() - for i := 0; i < count; i++ { + for range count { checkAddr = checkAddr.Next() } return prefix.Contains(checkAddr) diff --git a/stack_gvisor.go b/stack_gvisor.go index 63df41a6..03b28732 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -6,8 +6,10 @@ import ( "context" "net/netip" "runtime" + "sync" "time" + "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" "github.com/sagernet/gvisor/pkg/tcpip/header" @@ -40,6 +42,8 @@ type GVisor struct { logger logger.Logger stack *stack.Stack endpoint stack.LinkEndpoint + dispatcher *ForwardDispatcher + icmpForwarder *ICMPForwarder } type GVisorTun interface { @@ -88,23 +92,39 @@ func (t *GVisor) Start() error { if err != nil { return err } - linkEndpoint = &LinkEndpointFilter{linkEndpoint, t.broadcastAddr, t.tun} + if t.handler != nil { + t.dispatcher = NewForwardDispatcher(t.handler, &gvisorWriteback{tun: t.tun}, t.logger, t.udpTimeout, t.icmpTimeout) + } + linkEndpoint = &LinkEndpointFilter{ + LinkEndpoint: linkEndpoint, + BroadcastAddress: t.broadcastAddr, + Writer: t.tun, + Dispatcher: t.dispatcher, + Inet4Address: t.inet4Address, + Inet6Address: t.inet6Address, + Inet4LoopbackAddress: t.inet4LoopbackAddress, + Inet6LoopbackAddress: t.inet6LoopbackAddress, + } ipStack, err := newGVisorStack(linkEndpoint, nicOptions, false, true) if err != nil { return err } ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, NewTCPForwarderWithLoopback(t.ctx, ipStack, t.handler, t.inet4LoopbackAddress, t.inet6LoopbackAddress, t.tun).HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, NewUDPForwarder(t.ctx, ipStack, t.handler, t.udpTimeout).HandlePacket) - icmpForwarder := NewICMPForwarder(t.ctx, ipStack, t.handler, t.icmpTimeout) - icmpForwarder.SetLocalAddresses(t.inet4Address, t.inet6Address) + icmpForwarder := NewICMPForwarder(ipStack, t.handler, t.logger) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + t.icmpForwarder = icmpForwarder t.stack = ipStack t.endpoint = linkEndpoint return nil } func (t *GVisor) Close() error { + t.dispatcher.Close() + if t.icmpForwarder != nil { + t.icmpForwarder.Close() + } if t.stack == nil { return nil } @@ -116,6 +136,37 @@ func (t *GVisor) Close() error { return nil } +type gvisorWriteback struct { + tun GVisorTun + access sync.Mutex +} + +func (w *gvisorWriteback) ReturnHeadroom() int { + return 0 +} + +func (w *gvisorWriteback) WriteReturnPackets(packets [][]byte) error { + w.access.Lock() + defer w.access.Unlock() + var writeErrors []error + for _, packet := range packets { + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + }) + if header.IPVersion(packet) == header.IPv6Version { + pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber + } else { + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + } + _, err := w.tun.WritePacket(pkt) + pkt.DecRef() + if err != nil { + writeErrors = append(writeErrors, err) + } + } + return E.Errors(writeErrors...) +} + func AddressFromAddr(destination netip.Addr) tcpip.Address { if destination.Is6() { return tcpip.AddrFrom16(destination.As16()) diff --git a/stack_gvisor_filter.go b/stack_gvisor_filter.go index 18e46e8f..abdbcaa7 100644 --- a/stack_gvisor_filter.go +++ b/stack_gvisor_filter.go @@ -14,20 +14,39 @@ var _ stack.LinkEndpoint = (*LinkEndpointFilter)(nil) type LinkEndpointFilter struct { stack.LinkEndpoint - BroadcastAddress netip.Addr - Writer GVisorTun + BroadcastAddress netip.Addr + Writer GVisorTun + Dispatcher *ForwardDispatcher + Inet4Address netip.Addr + Inet6Address netip.Addr + Inet4LoopbackAddress []netip.Addr + Inet6LoopbackAddress []netip.Addr } func (w *LinkEndpointFilter) Attach(dispatcher stack.NetworkDispatcher) { - w.LinkEndpoint.Attach(&networkDispatcherFilter{dispatcher, w.BroadcastAddress, w.Writer}) + w.LinkEndpoint.Attach(&networkDispatcherFilter{ + NetworkDispatcher: dispatcher, + broadcastAddress: w.BroadcastAddress, + writer: w.Writer, + dispatcher: w.Dispatcher, + inet4Address: w.Inet4Address, + inet6Address: w.Inet6Address, + inet4LoopbackAddress: w.Inet4LoopbackAddress, + inet6LoopbackAddress: w.Inet6LoopbackAddress, + }) } var _ stack.NetworkDispatcher = (*networkDispatcherFilter)(nil) type networkDispatcherFilter struct { stack.NetworkDispatcher - broadcastAddress netip.Addr - writer GVisorTun + broadcastAddress netip.Addr + writer GVisorTun + dispatcher *ForwardDispatcher + inet4Address netip.Addr + inet6Address netip.Addr + inet4LoopbackAddress []netip.Addr + inet6LoopbackAddress []netip.Addr } func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) { @@ -50,5 +69,45 @@ func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkPro w.writer.WritePacket(pkt) return } + if w.dispatcher != nil && pkt.GSOOptions.Type == stack.GSONone && !pkt.GSOOptions.NeedsCsum { + if view, loaded := pkt.Data().PullUp(pkt.Data().Size()); loaded { + consumed := w.dispatch(protocol, destination, view) + w.dispatcher.Flush() + if consumed { + return + } + } + } w.NetworkDispatcher.DeliverNetworkPacket(protocol, pkt) } + +func (w *networkDispatcherFilter) dispatch(protocol tcpip.NetworkProtocolNumber, destination netip.Addr, view []byte) bool { + if protocol == header.IPv4ProtocolNumber { + switch header.IPv4(view).TransportProtocol() { + case header.TCPProtocolNumber: + for _, inet4LoopbackAddress := range w.inet4LoopbackAddress { + if destination == inet4LoopbackAddress { + return false + } + } + case header.ICMPv4ProtocolNumber: + if destination == w.inet4Address { + return false + } + } + } else { + switch header.IPv6(view).TransportProtocol() { + case header.TCPProtocolNumber: + for _, inet6LoopbackAddress := range w.inet6LoopbackAddress { + if destination == inet6LoopbackAddress { + return false + } + } + case header.ICMPv6ProtocolNumber: + if destination == w.inet6Address { + return false + } + } + } + return w.dispatcher.Dispatch(view) +} diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index da5549b6..11e82afe 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -3,10 +3,9 @@ package tun import ( - "context" - "errors" "net/netip" "sync" + "sync/atomic" "time" "github.com/sagernet/gvisor/pkg/buffer" @@ -17,37 +16,75 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" ) type ICMPForwarder struct { - ctx context.Context - stack *stack.Stack - inet4Address netip.Addr - inet6Address netip.Addr - handler Handler - mapping *DirectRouteMapping + stack *stack.Stack + handler Handler + logger logger.Logger + + returnPath icmpForwarderReturn + + flowAccess sync.Mutex + flows map[icmpFlowKey]*icmpFlow + lastSweep time.Time + attachedPorts map[Port]bool +} + +type icmpFlowKey struct { + v6 bool + source netip.Addr + destination netip.Addr + identifier uint16 } -func NewICMPForwarder( - ctx context.Context, - stack *stack.Stack, - handler Handler, - timeout time.Duration, -) *ICMPForwarder { - return &ICMPForwarder{ - ctx: ctx, - stack: stack, - handler: handler, - mapping: NewDirectRouteMapping(timeout), +type icmpFlow struct { + port Port + tracker FlowTracker + deadline time.Time + closed atomic.Bool +} + +func (f *icmpFlow) close(reason FlowCloseReason) { + if !f.closed.CompareAndSwap(false, true) { + return + } + if f.tracker != nil { + f.tracker.CloseFlow(reason) } } -func (f *ICMPForwarder) SetLocalAddresses(inet4Address, inet6Address netip.Addr) { - f.inet4Address = inet4Address - f.inet6Address = inet6Address +func (f *icmpFlow) CloseFlow() { + f.close(FlowCloseReset) +} + +func NewICMPForwarder(stack *stack.Stack, handler Handler, logger logger.Logger) *ICMPForwarder { + forwarder := &ICMPForwarder{ + stack: stack, + handler: handler, + logger: logger, + flows: make(map[icmpFlowKey]*icmpFlow), + attachedPorts: make(map[Port]bool), + } + forwarder.returnPath.forwarder = forwarder + return forwarder +} + +func (f *ICMPForwarder) Close() error { + f.returnPath.closed.Store(true) + f.flowAccess.Lock() + defer f.flowAccess.Unlock() + for key, flow := range f.flows { + flow.close(FlowCloseReset) + delete(f.flows, key) + } + for port := range f.attachedPorts { + port.DetachReturn(&f.returnPath) + delete(f.attachedPorts, port) + } + return nil } func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { @@ -57,32 +94,26 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { return false } - sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) - destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) - if destinationAddr != f.inet4Address { - action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return f.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &ICMPBackWriter{ - stack: f.stack, - packet: pkt, - source: ipHdr.SourceAddress(), - sourceNetwork: header.IPv4ProtocolNumber, - }, - timeout, - ) - }) - if errors.Is(err, ErrReset) { - gWriteUnreachable(f.stack, pkt) - return true - } else if errors.Is(err, ErrDrop) { - return true - } - if action != nil { - // TODO: handle error - _ = icmpWritePacketBuffer(action, pkt) + identifier := icmpHdr.Ident() + key := icmpFlowKey{ + source: AddrFromAddress(ipHdr.SourceAddress()), + destination: AddrFromAddress(ipHdr.DestinationAddress()), + identifier: identifier, + } + if f.forwardCached(key, pkt) { + return true + } + verdict := f.handler.JudgeFlow( + uint8(header.ICMPv4ProtocolNumber), + netip.AddrPortFrom(key.source, identifier), + netip.AddrPortFrom(key.destination, identifier), + nil, + ) + switch verdict.Action { + case ActionReject, ActionDrop: + return true + case ActionFlow: + if f.installFlow(key, verdict, pkt) { return true } } @@ -95,18 +126,18 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "get IPv4 network endpoint")) return true } route, gErr := f.stack.FindRoute( DefaultNIC, id.LocalAddress, id.RemoteAddress, - header.IPv6ProtocolNumber, + header.IPv4ProtocolNumber, false, ) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "find IPv4 route")) return true } defer route.Release() @@ -118,33 +149,27 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { return false } - sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) - destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) - if destinationAddr != f.inet6Address { - action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return f.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &ICMPBackWriter{ - stack: f.stack, - packet: pkt, - source: ipHdr.SourceAddress(), - sourceNetwork: header.IPv6ProtocolNumber, - }, - timeout, - ) - }) - if errors.Is(err, ErrReset) { - gWriteUnreachable(f.stack, pkt) - return true - } else if errors.Is(err, ErrDrop) { - return true - } - if action != nil { - // TODO: handle error - pkt.IncRef() - _ = icmpWritePacketBuffer(action, pkt) + identifier := icmpHdr.Ident() + key := icmpFlowKey{ + v6: true, + source: AddrFromAddress(ipHdr.SourceAddress()), + destination: AddrFromAddress(ipHdr.DestinationAddress()), + identifier: identifier, + } + if f.forwardCached(key, pkt) { + return true + } + verdict := f.handler.JudgeFlow( + uint8(header.ICMPv6ProtocolNumber), + netip.AddrPortFrom(key.source, identifier), + netip.AddrPortFrom(key.destination, identifier), + nil, + ) + switch verdict.Action { + case ActionReject, ActionDrop: + return true + case ActionFlow: + if f.installFlow(key, verdict, pkt) { return true } } @@ -159,9 +184,9 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa PayloadCsum: pkt.Data().Checksum(), PayloadLen: pkt.Data().Size(), })) - outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) + outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv6ProtocolNumber) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "get IPv6 network endpoint")) return true } route, gErr := f.stack.FindRoute( @@ -172,7 +197,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa false, ) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "find IPv6 route")) return true } defer route.Release() @@ -181,64 +206,234 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa } } -type ICMPBackWriter struct { - access sync.Mutex - stack *stack.Stack - packet *stack.PacketBuffer - source tcpip.Address - sourceNetwork tcpip.NetworkProtocolNumber +func (f *ICMPForwarder) forwardCached(key icmpFlowKey, pkt *stack.PacketBuffer) bool { + now := time.Now() + f.flowAccess.Lock() + flow, loaded := f.flows[key] + if loaded { + if flow.closed.Load() { + delete(f.flows, key) + loaded = false + } else if now.After(flow.deadline) { + delete(f.flows, key) + flow.close(FlowCloseTimeout) + loaded = false + } else { + flow.deadline = now.Add(defaultICMPTimeout) + } + } + f.flowAccess.Unlock() + if !loaded { + return false + } + f.writeToPort(flow, pkt) + return true } -func (w *ICMPBackWriter) WritePacket(p []byte) error { - if w.sourceNetwork == header.IPv4ProtocolNumber { - route, err := w.stack.FindRoute( - DefaultNIC, - header.IPv4(p).SourceAddress(), - w.source, - w.sourceNetwork, - false, - ) +func (f *ICMPForwarder) installFlow(key icmpFlowKey, verdict FlowVerdict, pkt *stack.PacketBuffer) bool { + port := verdict.Port + if port == nil { + return false + } + inet4Address, inet6Address := port.PortAddresses() + portAddress := inet4Address + if key.v6 { + portAddress = inet6Address + } + if !portAddress.IsValid() || !portAddress.IsUnspecified() { + return false + } + f.flowAccess.Lock() + if !f.attachedPorts[port] { + err := port.AttachReturn(&f.returnPath) if err != nil { - return gonet.TranslateNetstackError(err) + f.flowAccess.Unlock() + f.logger.Trace(E.Cause(err, "attach ICMP return path")) + return false } - defer route.Release() - packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: buffer.MakeWithData(p), - }) - defer packet.DecRef() - parse.IPv4(packet) - err = route.WritePacketDirect(packet) - if err != nil { - return gonet.TranslateNetstackError(err) + f.attachedPorts[port] = true + } + now := time.Now() + if now.Sub(f.lastSweep) >= defaultICMPTimeout { + f.lastSweep = now + for flowKey, cachedFlow := range f.flows { + if cachedFlow.closed.Load() { + delete(f.flows, flowKey) + } else if now.After(cachedFlow.deadline) { + delete(f.flows, flowKey) + cachedFlow.close(FlowCloseTimeout) + } } - } else { - route, err := w.stack.FindRoute( - DefaultNIC, - header.IPv6(p).SourceAddress(), - w.source, - w.sourceNetwork, - false, - ) - if err != nil { - return gonet.TranslateNetstackError(err) + } + flow := &icmpFlow{port: port, deadline: now.Add(defaultICMPTimeout)} + if verdict.NewTracker != nil { + flow.tracker = verdict.NewTracker() + } + f.flows[key] = flow + f.flowAccess.Unlock() + if flow.tracker != nil { + flow.tracker.AttachFlow(flow) + } + f.writeToPort(flow, pkt) + return true +} + +func (f *ICMPForwarder) writeToPort(flow *icmpFlow, pkt *stack.PacketBuffer) { + networkSlice := pkt.NetworkHeader().Slice() + transportSlice := pkt.TransportHeader().Slice() + dataSlice := pkt.Data().AsRange().ToSlice() + packetSlice := make([]byte, 0, len(networkSlice)+len(transportSlice)+len(dataSlice)) + packetSlice = append(packetSlice, networkSlice...) + packetSlice = append(packetSlice, transportSlice...) + packetSlice = append(packetSlice, dataSlice...) + if flow.tracker != nil { + flow.tracker.CountForward(len(packetSlice)) + } + err := flow.port.WritePackets([][]byte{packetSlice}) + if err != nil { + f.logger.Trace(E.Cause(err, "forward ICMP packet")) + } +} + +func (f *ICMPForwarder) lookupFlow(key icmpFlowKey) *icmpFlow { + f.flowAccess.Lock() + defer f.flowAccess.Unlock() + flow, loaded := f.flows[key] + if !loaded { + return nil + } + if flow.closed.Load() { + delete(f.flows, key) + return nil + } + now := time.Now() + if now.After(flow.deadline) { + delete(f.flows, key) + flow.close(FlowCloseTimeout) + return nil + } + flow.deadline = now.Add(defaultICMPTimeout) + return flow +} + +type icmpForwarderReturn struct { + forwarder *ICMPForwarder + closed atomic.Bool +} + +func (r *icmpForwarderReturn) ReturnHeadroom() int { + return 0 +} + +func (r *icmpForwarderReturn) ReturnPackets(packets [][]byte) [][]byte { + if r.closed.Load() { + return packets + } + unconsumed := packets[:0] + for _, packet := range packets { + if !r.forwarder.returnPacket(packet) { + unconsumed = append(unconsumed, packet) } - defer route.Release() - packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: buffer.MakeWithData(p), - }) - parse.IPv6(packet) - defer packet.DecRef() - err = route.WritePacketDirect(packet) - if err != nil { - return gonet.TranslateNetstackError(err) + } + return unconsumed +} + +func (f *ICMPForwarder) returnPacket(packet []byte) bool { + if len(packet) == 0 { + return false + } + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv4ProtocolNumber || len(ipHdr.Payload()) < header.ICMPv4MinimumSize { + return false + } + icmpHdr := header.ICMPv4(ipHdr.Payload()) + var key icmpFlowKey + switch icmpHdr.Type() { + case header.ICMPv4EchoReply: + key = icmpFlowKey{ + source: AddrFromAddress(ipHdr.DestinationAddress()), + destination: AddrFromAddress(ipHdr.SourceAddress()), + identifier: icmpHdr.Ident(), + } + case header.ICMPv4TimeExceeded, header.ICMPv4DstUnreachable: + inner := icmpHdr.Payload() + if len(inner) < header.IPv4MinimumSize { + return false + } + innerIPHdr := header.IPv4(inner) + innerHeaderLength := int(innerIPHdr.HeaderLength()) + if innerHeaderLength < header.IPv4MinimumSize || len(inner) < innerHeaderLength+header.ICMPv4MinimumSize { + return false + } + if innerIPHdr.TransportProtocol() != header.ICMPv4ProtocolNumber { + return false + } + innerICMPHdr := header.ICMPv4(inner[innerHeaderLength:]) + key = icmpFlowKey{ + source: AddrFromAddress(innerIPHdr.SourceAddress()), + destination: AddrFromAddress(innerIPHdr.DestinationAddress()), + identifier: innerICMPHdr.Ident(), + } + default: + return false + } + flow := f.lookupFlow(key) + if flow == nil { + return false + } + if flow.tracker != nil { + flow.tracker.CountReverse(len(packet)) + } + return f.writeBack(packet, header.IPv4ProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress()) + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv6ProtocolNumber || len(ipHdr.Payload()) < header.ICMPv6MinimumSize { + return false } + icmpHdr := header.ICMPv6(ipHdr.Payload()) + if icmpHdr.Type() != header.ICMPv6EchoReply { + return false + } + key := icmpFlowKey{ + v6: true, + source: AddrFromAddress(ipHdr.DestinationAddress()), + destination: AddrFromAddress(ipHdr.SourceAddress()), + identifier: icmpHdr.Ident(), + } + flow := f.lookupFlow(key) + if flow == nil { + return false + } + if flow.tracker != nil { + flow.tracker.CountReverse(len(packet)) + } + return f.writeBack(packet, header.IPv6ProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress()) + default: + return false } - return nil } -func icmpWritePacketBuffer(action DirectRouteDestination, packetBuffer *stack.PacketBuffer) error { - packetSlice := packetBuffer.NetworkHeader().Slice() - packetSlice = append(packetSlice, packetBuffer.TransportHeader().Slice()...) - packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) - return action.WritePacket(buf.As(packetSlice).ToOwned()) +func (f *ICMPForwarder) writeBack(packet []byte, protocol tcpip.NetworkProtocolNumber, localAddress tcpip.Address, remoteAddress tcpip.Address) bool { + route, gErr := f.stack.FindRoute(DefaultNIC, localAddress, remoteAddress, protocol, false) + if gErr != nil { + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "find route for ICMP reply")) + return true + } + defer route.Release() + packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + }) + defer packetBuffer.DecRef() + if protocol == header.IPv4ProtocolNumber { + parse.IPv4(packetBuffer) + } else { + parse.IPv6(packetBuffer) + } + gErr = route.WritePacketDirect(packetBuffer) + if gErr != nil { + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "write ICMP reply")) + } + return true } diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index f5e2e6e6..96d8897a 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -4,7 +4,6 @@ package tun import ( "context" - "errors" "net" "os" "sync" @@ -19,7 +18,7 @@ import ( ) type gLazyConn struct { - tcpConn *gonet.TCPConn + tcpConn *gTCPConn parentCtx context.Context stack *stack.Stack request *tcp.ForwarderRequest @@ -31,9 +30,6 @@ type gLazyConn struct { } func (c *gLazyConn) HandshakeContext(ctx context.Context) error { - if c.handshakeDone { - return c.handshakeErr - } c.handshakeAccess.Lock() defer c.handshakeAccess.Unlock() if c.handshakeDone { @@ -46,16 +42,22 @@ func (c *gLazyConn) HandshakeContext(ctx context.Context) error { wq waiter.Queue endpoint tcpip.Endpoint ) - handshakeCtx, cancel := context.WithCancel(ctx) - go func() { - select { - case <-c.parentCtx.Done(): - wq.Notify(wq.Events()) - case <-handshakeCtx.Done(): - } - }() + var cancel context.CancelFunc + if parentDone := c.parentCtx.Done(); parentDone != nil { + var handshakeCtx context.Context + handshakeCtx, cancel = context.WithCancel(ctx) + go func() { + select { + case <-parentDone: + wq.Notify(wq.Events()) + case <-handshakeCtx.Done(): + } + }() + } endpoint, err := c.request.CreateEndpoint(&wq) - cancel() + if cancel != nil { + cancel() + } if err != nil { gErr := gonet.TranslateNetstackError(err) c.handshakeErr = gErr @@ -66,21 +68,18 @@ func (c *gLazyConn) HandshakeContext(ctx context.Context) error { endpoint.SocketOptions().SetKeepAlive(true) endpoint.SetSockOpt(common.Ptr(tcpip.KeepaliveIdleOption(15 * time.Second))) endpoint.SetSockOpt(common.Ptr(tcpip.KeepaliveIntervalOption(15 * time.Second))) - tcpConn := gonet.NewTCPConn(&wq, endpoint) + tcpConn := newGTCPConn(&wq, endpoint, c.localAddr, c.remoteAddr) c.tcpConn = tcpConn return nil } func (c *gLazyConn) HandshakeFailure(err error) error { - if c.handshakeDone { - return os.ErrInvalid - } c.handshakeAccess.Lock() defer c.handshakeAccess.Unlock() if c.handshakeDone { return os.ErrInvalid } - c.request.Complete(!errors.Is(err, ErrDrop)) + c.request.Complete(true) c.handshakeDone = true c.handshakeErr = err return nil @@ -90,6 +89,18 @@ func (c *gLazyConn) HandshakeSuccess() error { return c.HandshakeContext(context.Background()) } +func (c *gLazyConn) NeedHandshakeForRead() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() + return !c.handshakeDone +} + +func (c *gLazyConn) NeedHandshakeForWrite() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() + return !c.handshakeDone +} + func (c *gLazyConn) Read(b []byte) (n int, err error) { err = c.HandshakeContext(context.Background()) if err != nil { @@ -139,57 +150,38 @@ func (c *gLazyConn) SetWriteDeadline(t time.Time) error { } func (c *gLazyConn) Close() error { - if !c.handshakeDone { - c.handshakeAccess.Lock() - if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - c.handshakeDone = true - return nil - } else if c.handshakeErr != nil { - return nil - } - c.handshakeAccess.Unlock() - } else if c.handshakeErr != nil { + if c.closeBeforeHandshake() { return nil } return c.tcpConn.Close() } func (c *gLazyConn) CloseRead() error { - if !c.handshakeDone { - c.handshakeAccess.Lock() - if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - c.handshakeDone = true - return nil - } else if c.handshakeErr != nil { - return nil - } - c.handshakeAccess.Unlock() - } else if c.handshakeErr != nil { + if c.closeBeforeHandshake() { return nil } return c.tcpConn.CloseRead() } func (c *gLazyConn) CloseWrite() error { + if c.closeBeforeHandshake() { + return nil + } + return c.tcpConn.CloseWrite() +} + +func (c *gLazyConn) closeBeforeHandshake() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() if !c.handshakeDone { - c.handshakeAccess.Lock() - if !c.handshakeDone { + if c.request != nil { c.request.Complete(true) - c.handshakeErr = net.ErrClosed - c.handshakeDone = true - return nil - } else if c.handshakeErr != nil { - return nil } - c.handshakeAccess.Unlock() - } else if c.handshakeErr != nil { - return nil + c.handshakeErr = net.ErrClosed + c.handshakeDone = true + return true } - return c.tcpConn.CloseRead() + return c.handshakeErr != nil } func (c *gLazyConn) ReaderReplaceable() bool { diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 0c63ee11..f432e287 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -4,17 +4,15 @@ package tun import ( "context" - "errors" "net/netip" "github.com/sagernet/gvisor/pkg/tcpip" "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/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" ) type TCPForwarder struct { @@ -79,9 +77,12 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func (f *TCPForwarder) Forward(r *tcp.ForwarderRequest) { source := M.SocksaddrFrom(AddrFromAddress(r.ID().RemoteAddress), r.ID().RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(r.ID().LocalAddress), r.ID().LocalPort) - _, pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination, nil, 0) - if pErr != nil { - r.Complete(!errors.Is(pErr, ErrDrop)) + switch f.handler.JudgeFlow(uint8(header.TCPProtocolNumber), source.AddrPort(), destination.AddrPort(), nil).Action { + case ActionReject: + r.Complete(true) + return + case ActionDrop: + r.Complete(false) return } conn := &gLazyConn{ diff --git a/stack_gvisor_tcp_conn.go b/stack_gvisor_tcp_conn.go new file mode 100644 index 00000000..ad48d42f --- /dev/null +++ b/stack_gvisor_tcp_conn.go @@ -0,0 +1,325 @@ +//go:build with_gvisor + +package tun + +import ( + "bytes" + "errors" + "io" + "net" + "os" + "time" + + "github.com/sagernet/gvisor/pkg/sync" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/waiter" + "github.com/sagernet/sing/common/buf" + N "github.com/sagernet/sing/common/network" +) + +var ( + _ net.Conn = (*gTCPConn)(nil) + _ N.ReadWaiter = (*gTCPConn)(nil) +) + +type gTCPConn struct { + gTCPDeadline + + wq *waiter.Queue + ep tcpip.Endpoint + + localAddr net.Addr + remoteAddr net.Addr + + readMu sync.Mutex + readWaitOption N.ReadWaitOptions +} + +func newGTCPConn(wq *waiter.Queue, ep tcpip.Endpoint, localAddr net.Addr, remoteAddr net.Addr) *gTCPConn { + conn := &gTCPConn{ + wq: wq, + ep: ep, + localAddr: localAddr, + remoteAddr: remoteAddr, + } + conn.gTCPDeadline.init() + return conn +} + +func (c *gTCPConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOption = options + return false +} + +func (c *gTCPConn) WaitReadBuffer() (*buf.Buffer, error) { + c.readMu.Lock() + defer c.readMu.Unlock() + + deadline := c.readCancel() + for { + if err := c.waitReadable(deadline); err != nil { + return nil, err + } + buffer := c.readWaitOption.NewBuffer() + writer := tcpip.SliceWriter(buffer.FreeBytes()) + result, err := c.ep.Read(&writer, tcpip.ReadOptions{}) + if _, wouldBlock := err.(*tcpip.ErrWouldBlock); wouldBlock { + buffer.Release() + continue + } + if err != nil { + buffer.Release() + return nil, c.translateReadError(err) + } + if result.Count == 0 { + buffer.Release() + continue + } + buffer.Truncate(result.Count) + c.readWaitOption.PostReturn(buffer) + c.ep.ModerateRecvBuf(result.Count) + return buffer, nil + } +} + +func (c *gTCPConn) Read(b []byte) (int, error) { + c.readMu.Lock() + defer c.readMu.Unlock() + + writer := tcpip.SliceWriter(b) + n, err := c.readTo(&writer, c.readCancel()) + if n != 0 { + c.ep.ModerateRecvBuf(n) + } + return n, err +} + +func (c *gTCPConn) readTo(writer io.Writer, deadline <-chan struct{}) (int, error) { + select { + case <-deadline: + return 0, c.newOpError("read", os.ErrDeadlineExceeded) + default: + } + + result, err := c.ep.Read(writer, tcpip.ReadOptions{}) + if _, wouldBlock := err.(*tcpip.ErrWouldBlock); wouldBlock { + waitEntry, notifyCh := waiter.NewChannelEntry(waiter.ReadableEvents) + c.wq.EventRegister(&waitEntry) + defer c.wq.EventUnregister(&waitEntry) + for { + result, err = c.ep.Read(writer, tcpip.ReadOptions{}) + if _, wouldBlock = err.(*tcpip.ErrWouldBlock); !wouldBlock { + break + } + select { + case <-deadline: + return 0, c.newOpError("read", os.ErrDeadlineExceeded) + case <-notifyCh: + } + } + } + + if err != nil { + return 0, c.translateReadError(err) + } + return result.Count, nil +} + +func (c *gTCPConn) waitReadable(deadline <-chan struct{}) error { + select { + case <-deadline: + return c.newOpError("read", os.ErrDeadlineExceeded) + default: + } + if c.ep.Readiness(waiter.ReadableEvents)&waiter.ReadableEvents != 0 { + return nil + } + + waitEntry, notifyCh := waiter.NewChannelEntry(waiter.ReadableEvents) + c.wq.EventRegister(&waitEntry) + defer c.wq.EventUnregister(&waitEntry) + for c.ep.Readiness(waiter.ReadableEvents)&waiter.ReadableEvents == 0 { + select { + case <-deadline: + return c.newOpError("read", os.ErrDeadlineExceeded) + case <-notifyCh: + } + } + return nil +} + +func (c *gTCPConn) translateReadError(err tcpip.Error) error { + if _, closed := err.(*tcpip.ErrClosedForReceive); closed { + return io.EOF + } + return c.newOpError("read", gonet.TranslateNetstackError(err)) +} + +func (c *gTCPConn) Write(b []byte) (int, error) { + deadline := c.writeCancel() + + select { + case <-deadline: + return 0, c.newOpError("write", os.ErrDeadlineExceeded) + default: + } + + var ( + reader bytes.Reader + nBytes int + entry waiter.Entry + ch <-chan struct{} + ) + for nBytes != len(b) { + reader.Reset(b[nBytes:]) + n, err := c.ep.Write(&reader, tcpip.WriteOptions{}) + nBytes += int(n) + switch err.(type) { + case nil: + case *tcpip.ErrWouldBlock: + if ch == nil { + entry, ch = waiter.NewChannelEntry(waiter.WritableEvents) + c.wq.EventRegister(&entry) + defer c.wq.EventUnregister(&entry) + } else { + select { + case <-deadline: + return nBytes, c.newOpError("write", os.ErrDeadlineExceeded) + case <-ch: + continue + } + } + default: + return nBytes, c.newOpError("write", gonet.TranslateNetstackError(err)) + } + } + return nBytes, nil +} + +func (c *gTCPConn) Close() error { + c.ep.Close() + return nil +} + +func (c *gTCPConn) CloseRead() error { + if err := c.ep.Shutdown(tcpip.ShutdownRead); err != nil { + return c.newOpError("close", errors.New(err.String())) + } + return nil +} + +func (c *gTCPConn) CloseWrite() error { + if err := c.ep.Shutdown(tcpip.ShutdownWrite); err != nil { + return c.newOpError("close", errors.New(err.String())) + } + return nil +} + +func (c *gTCPConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *gTCPConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *gTCPConn) SetDeadline(t time.Time) error { + return c.gTCPDeadline.SetDeadline(t) +} + +func (c *gTCPConn) SetReadDeadline(t time.Time) error { + return c.gTCPDeadline.SetReadDeadline(t) +} + +func (c *gTCPConn) SetWriteDeadline(t time.Time) error { + return c.gTCPDeadline.SetWriteDeadline(t) +} + +func (c *gTCPConn) newOpError(op string, err error) *net.OpError { + return &net.OpError{ + Op: op, + Net: "tcp", + Source: c.localAddr, + Addr: c.remoteAddr, + Err: err, + } +} + +type gTCPDeadline struct { + mu sync.Mutex + + readTimer *time.Timer + readCancelCh chan struct{} + writeTimer *time.Timer + writeCancelCh chan struct{} +} + +func (d *gTCPDeadline) init() { + d.readCancelCh = make(chan struct{}) + d.writeCancelCh = make(chan struct{}) +} + +func (d *gTCPDeadline) readCancel() <-chan struct{} { + d.mu.Lock() + cancelCh := d.readCancelCh + d.mu.Unlock() + return cancelCh +} + +func (d *gTCPDeadline) writeCancel() <-chan struct{} { + d.mu.Lock() + cancelCh := d.writeCancelCh + d.mu.Unlock() + return cancelCh +} + +func (d *gTCPDeadline) SetDeadline(t time.Time) error { + d.mu.Lock() + d.setDeadline(&d.readCancelCh, &d.readTimer, t) + d.setDeadline(&d.writeCancelCh, &d.writeTimer, t) + d.mu.Unlock() + return nil +} + +func (d *gTCPDeadline) SetReadDeadline(t time.Time) error { + d.mu.Lock() + d.setDeadline(&d.readCancelCh, &d.readTimer, t) + d.mu.Unlock() + return nil +} + +func (d *gTCPDeadline) SetWriteDeadline(t time.Time) error { + d.mu.Lock() + d.setDeadline(&d.writeCancelCh, &d.writeTimer, t) + d.mu.Unlock() + return nil +} + +func (d *gTCPDeadline) setDeadline(cancelCh *chan struct{}, timer **time.Timer, t time.Time) { + if *timer != nil && !(*timer).Stop() { + *cancelCh = make(chan struct{}) + } + + select { + case <-*cancelCh: + *cancelCh = make(chan struct{}) + default: + } + + if t.IsZero() { + *timer = nil + return + } + + timeout := time.Until(t) + if timeout <= 0 { + close(*cancelCh) + return + } + + ch := *cancelCh + *timer = time.AfterFunc(timeout, func() { + close(ch) + }) +} diff --git a/stack_gvisor_tcpbuf_default.go b/stack_gvisor_tcpbuf_default.go index f636d1a4..fc3bc2b6 100644 --- a/stack_gvisor_tcpbuf_default.go +++ b/stack_gvisor_tcpbuf_default.go @@ -9,10 +9,10 @@ import "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" const ( tcpRXBufMinSize = tcp.MinBufferSize - tcpRXBufDefSize = tcp.DefaultSendBufferSize + tcpRXBufDefSize = tcp.DefaultReceiveBufferSize tcpRXBufMaxSize = 8 << 20 // 8MiB tcpTXBufMinSize = tcp.MinBufferSize - tcpTXBufDefSize = tcp.DefaultReceiveBufferSize + tcpTXBufDefSize = tcp.DefaultSendBufferSize tcpTXBufMaxSize = 6 << 20 // 6MiB ) diff --git a/stack_gvisor_tcpbuf_ios.go b/stack_gvisor_tcpbuf_ios.go index 495e59bf..6704c9d6 100644 --- a/stack_gvisor_tcpbuf_ios.go +++ b/stack_gvisor_tcpbuf_ios.go @@ -12,10 +12,10 @@ const ( // unchanged on iOS for now as to not increase pressure towards the // NetworkExtension memory limit. tcpRXBufMinSize = tcp.MinBufferSize - tcpRXBufDefSize = tcp.DefaultSendBufferSize + tcpRXBufDefSize = tcp.DefaultReceiveBufferSize tcpRXBufMaxSize = tcp.MaxBufferSize tcpTXBufMinSize = tcp.MinBufferSize - tcpTXBufDefSize = tcp.DefaultReceiveBufferSize + tcpTXBufDefSize = tcp.DefaultSendBufferSize tcpTXBufMaxSize = tcp.MaxBufferSize ) diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index f91a2b3e..2ae54cf9 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -4,7 +4,6 @@ package tun import ( "context" - "errors" "math" "net/netip" "os" @@ -38,19 +37,16 @@ func NewUDPForwarder(ctx context.Context, stack *stack.Stack, handler Handler, t stack: stack, handler: handler, } - forwarder.udpNat = udpnat.New(handler, forwarder.PreparePacketConnection, timeout, true) + forwarder.udpNat = udpnat.New(handler, forwarder.PreparePacketConnection, timeout, false) return forwarder } func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { source := M.SocksaddrFrom(AddrFromAddress(id.RemoteAddress), id.RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(id.LocalAddress), id.LocalPort) - bufferRange := pkt.Data().AsRange() - var bufferSlices [][]byte - rangeIterate(bufferRange, func(view *buffer.View) { - bufferSlices = append(bufferSlices, view.AsSlice()) - }) - f.udpNat.NewPacket(bufferSlices, source, destination, pkt) + data := pkt.Data() + payload, _ := data.PullUp(data.Size()) + f.udpNat.NewPacket([][]byte{payload}, source, destination, pkt) return true } @@ -58,11 +54,20 @@ func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func rangeIterate(r stack.Range, fn func(*buffer.View)) func (f *UDPForwarder) PreparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - _, pErr := f.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) - if pErr != nil { - if !errors.Is(pErr, ErrDrop) { - gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) + firstPacketBuffer := userData.(*stack.PacketBuffer) + var firstPacket []byte + rangeIterate(firstPacketBuffer.Data().AsRange(), func(view *buffer.View) { + if firstPacket == nil { + firstPacket = view.AsSlice() + } else { + firstPacket = append(firstPacket[:len(firstPacket):len(firstPacket)], view.AsSlice()...) } + }) + switch f.handler.JudgeFlow(uint8(header.UDPProtocolNumber), source.AddrPort(), destination.AddrPort(), firstPacket).Action { + case ActionReject: + gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) + return false, nil, nil, nil + case ActionDrop: return false, nil, nil, nil } var sourceNetwork tcpip.NetworkProtocolNumber diff --git a/stack_mixed.go b/stack_mixed.go index 8836d6ba..46803807 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -12,7 +12,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/link/channel" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" ) @@ -73,8 +73,6 @@ func (m *Mixed) tunLoop() { return } if linuxTUN, isLinuxTUN := m.tun.(LinuxTUN); isLinuxTUN { - m.frontHeadroom = linuxTUN.FrontHeadroom() - m.txChecksumOffload = linuxTUN.TXChecksumOffload() batchSize := linuxTUN.BatchSize() if batchSize > 1 { m.batchLoopLinux(linuxTUN, batchSize) @@ -105,6 +103,7 @@ func (m *Mixed) tunLoop() { m.logger.Trace(E.Cause(err, "write packet")) } } + m.dispatcher.Flush() } } @@ -124,13 +123,14 @@ func (m *Mixed) wintunLoop(winTun WinTun) { m.logger.Trace(E.Cause(err, "write packet")) } } + m.dispatcher.Flush() release() } } func (m *Mixed) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { packetBuffers := make([][]byte, batchSize) - writeBuffers := make([][]byte, batchSize) + writeBuffers := make([][]byte, 0, batchSize) packetSizes := make([]int, batchSize) for i := range packetBuffers { packetBuffers[i] = make([]byte, m.mtu+PacketOffset+m.frontHeadroom) @@ -164,11 +164,13 @@ func (m *Mixed) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { } writeBuffers = writeBuffers[:0] } + m.dispatcher.Flush() } } func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { var writeBuffers []*buf.Buffer + var releaseBuffers []*buf.Buffer for { buffers, err := darwinTUN.BatchRead() if err != nil { @@ -181,6 +183,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { continue } writeBuffers = writeBuffers[:0] + releaseBuffers = releaseBuffers[:0] for _, buffer := range buffers { packetSize := buffer.Len() if packetSize < header.IPv4MinimumSize { @@ -190,7 +193,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { if m.processPacket(buffer.Bytes()) { writeBuffers = append(writeBuffers, buffer) } else { - buffer.Release() + releaseBuffers = append(releaseBuffers, buffer) } } if len(writeBuffers) > 0 { @@ -200,6 +203,8 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { } buf.ReleaseMulti(writeBuffers) } + m.dispatcher.Flush() + buf.ReleaseMulti(releaseBuffers) } } @@ -229,6 +234,9 @@ func (m *Mixed) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { if destination == m.broadcastAddr || !destination.IsGlobalUnicast() { return } + if m.dispatchIPv4(ipHdr, destination) { + return false, nil + } switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: writeBack, err = m.processIPv4TCP(ipHdr, ipHdr.Payload()) @@ -249,9 +257,13 @@ func (m *Mixed) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { func (m *Mixed) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { writeBack = true - if !ipHdr.DestinationAddr().IsGlobalUnicast() { + destination := ipHdr.DestinationAddr() + if !destination.IsGlobalUnicast() { return } + if m.dispatchIPv6(ipHdr, destination) { + return false, nil + } switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: writeBack, err = m.processIPv6TCP(ipHdr, ipHdr.Payload()) diff --git a/stack_system.go b/stack_system.go index 030eee17..3cb0cb04 100644 --- a/stack_system.go +++ b/stack_system.go @@ -5,11 +5,13 @@ import ( "errors" "net" "net/netip" + "slices" "syscall" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" @@ -46,7 +48,7 @@ type System struct { tcpPort6 uint16 tcpNat *TCPNat udpNat *udpnat.Service - directNat *DirectRouteMapping + dispatcher *ForwardDispatcher bindInterface bool interfaceFinder control.InterfaceFinder frontHeadroom int @@ -101,6 +103,7 @@ func NewSystem(options StackOptions) (Stack, error) { } func (s *System) Close() error { + s.dispatcher.Close() return common.Close( s.tcpListener, s.tcpListener6, @@ -131,7 +134,7 @@ func (s *System) start() error { var tcpListener net.Listener var err error if s.inet4NextAddress.IsValid() { - for i := 0; i < 3; i++ { + for range 3 { tcpListener, err = listener.Listen(s.ctx, "tcp4", net.JoinHostPort(s.inet4Address.String(), "0")) if !retryableListenError(err) { break @@ -146,7 +149,7 @@ func (s *System) start() error { go s.acceptLoop(tcpListener) } if s.inet6NextAddress.IsValid() { - for i := 0; i < 3; i++ { + for range 3 { tcpListener, err = listener.Listen(s.ctx, "tcp6", net.JoinHostPort(s.inet6Address.String(), "0")) if !retryableListenError(err) { break @@ -162,7 +165,13 @@ func (s *System) start() error { } s.tcpNat = NewNat(s.ctx, s.udpTimeout) s.udpNat = udpnat.New(s.handler, s.preparePacketConnection, s.udpTimeout, false) - s.directNat = NewDirectRouteMapping(s.icmpTimeout) + if linuxTUN, isLinuxTUN := s.tun.(LinuxTUN); isLinuxTUN { + s.frontHeadroom = linuxTUN.FrontHeadroom() + s.txChecksumOffload = linuxTUN.TXChecksumOffload() + } + if s.handler != nil { + s.dispatcher = NewForwardDispatcher(s.handler, newSystemWriteback(s.tun, s.frontHeadroom), s.logger, s.udpTimeout, s.icmpTimeout) + } return nil } @@ -172,8 +181,6 @@ func (s *System) tunLoop() { return } if linuxTUN, isLinuxTUN := s.tun.(LinuxTUN); isLinuxTUN { - s.frontHeadroom = linuxTUN.FrontHeadroom() - s.txChecksumOffload = linuxTUN.TXChecksumOffload() batchSize := linuxTUN.BatchSize() if batchSize > 1 { s.batchLoopLinux(linuxTUN, batchSize) @@ -204,6 +211,7 @@ func (s *System) tunLoop() { s.logger.Trace(E.Cause(err, "write packet")) } } + s.dispatcher.Flush() } } @@ -223,13 +231,14 @@ func (s *System) wintunLoop(winTun WinTun) { s.logger.Trace(E.Cause(err, "write packet")) } } + s.dispatcher.Flush() release() } } func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { packetBuffers := make([][]byte, batchSize) - writeBuffers := make([][]byte, batchSize) + writeBuffers := make([][]byte, 0, batchSize) packetSizes := make([]int, batchSize) for i := range packetBuffers { packetBuffers[i] = make([]byte, s.mtu+s.frontHeadroom) @@ -245,7 +254,7 @@ func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { if n == 0 { continue } - for i := 0; i < n; i++ { + for i := range n { packetSize := packetSizes[i] if packetSize < header.IPv4MinimumSize { continue @@ -263,11 +272,13 @@ func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { } writeBuffers = writeBuffers[:0] } + s.dispatcher.Flush() } } func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { var writeBuffers []*buf.Buffer + var releaseBuffers []*buf.Buffer for { buffers, err := darwinTUN.BatchRead() if err != nil { @@ -280,6 +291,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { continue } writeBuffers = writeBuffers[:0] + releaseBuffers = releaseBuffers[:0] for _, buffer := range buffers { packetSize := buffer.Len() if packetSize < header.IPv4MinimumSize { @@ -289,7 +301,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { if s.processPacket(buffer.Bytes()) { writeBuffers = append(writeBuffers, buffer) } else { - buffer.Release() + releaseBuffers = append(releaseBuffers, buffer) } } if len(writeBuffers) > 0 { @@ -299,6 +311,8 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { } buf.ReleaseMulti(writeBuffers) } + s.dispatcher.Flush() + buf.ReleaseMulti(releaseBuffers) } } @@ -338,11 +352,53 @@ func (s *System) acceptLoop(listener net.Listener) { } } +func (s *System) dispatchIPv4(ipHdr header.IPv4, destination netip.Addr) bool { + switch ipHdr.TransportProtocol() { + case header.TCPProtocolNumber: + if slices.Contains(s.inet4LoopbackAddress, destination) { + return false + } + if ipHdr.SourceAddr() == s.inet4Address && + ipHdr.FragmentOffset() == 0 && + len(ipHdr.Payload()) >= header.TCPMinimumSize && + header.TCP(ipHdr.Payload()).SourcePort() == s.tcpPort { + return false + } + case header.ICMPv4ProtocolNumber: + if destination == s.inet4Address { + return false + } + } + return s.dispatcher.Dispatch(ipHdr) +} + +func (s *System) dispatchIPv6(ipHdr header.IPv6, destination netip.Addr) bool { + switch ipHdr.TransportProtocol() { + case header.TCPProtocolNumber: + if slices.Contains(s.inet6LoopbackAddress, destination) { + return false + } + if ipHdr.SourceAddr() == s.inet6Address && + len(ipHdr.Payload()) >= header.TCPMinimumSize && + header.TCP(ipHdr.Payload()).SourcePort() == s.tcpPort6 { + return false + } + case header.ICMPv6ProtocolNumber: + if destination == s.inet6Address { + return false + } + } + return s.dispatcher.Dispatch(ipHdr) +} + func (s *System) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { destination := ipHdr.DestinationAddr() if destination == s.broadcastAddr || !destination.IsGlobalUnicast() { return } + if s.dispatchIPv4(ipHdr, destination) { + return false, nil + } writeBack = true switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: @@ -360,9 +416,13 @@ func (s *System) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { } func (s *System) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { - if !ipHdr.DestinationAddr().IsGlobalUnicast() { + destination := ipHdr.DestinationAddr() + if !destination.IsGlobalUnicast() { return } + if s.dispatchIPv6(ipHdr, destination) { + return false, nil + } writeBack = true switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: @@ -389,91 +449,33 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err if session == nil { return false, E.New("ipv4: tcp: session not found: ", destination.Port()) } - ipHdr.SetSourceAddr(session.Destination.Addr()) - tcpHdr.SetSourcePort(session.Destination.Port()) - ipHdr.SetDestinationAddr(session.Source.Addr()) - tcpHdr.SetDestinationPort(session.Source.Port()) + rewriteIPv4TCP(ipHdr, tcpHdr, s.txChecksumOffload, + session.Destination.Addr(), session.Destination.Port(), true, + session.Source.Addr(), session.Source.Port(), true) } else { var loopback bool for _, inet4LoopbackAddress := range s.inet4LoopbackAddress { if destination.Addr() == inet4LoopbackAddress { - ipHdr.SetDestinationAddr(ipHdr.SourceAddr()) - ipHdr.SetSourceAddr(inet4LoopbackAddress) + rewriteIPv4TCP(ipHdr, tcpHdr, s.txChecksumOffload, + inet4LoopbackAddress, 0, false, + source.Addr(), 0, false) loopback = true break } } if !loopback { - natPort, err := s.tcpNat.Lookup(source, destination, s.handler) - if err != nil { - if errors.Is(err, ErrDrop) { - return false, nil - } else { - return false, s.resetIPv4TCP(ipHdr, tcpHdr) - } + natPort := s.tcpNat.Lookup(source, destination) + if natPort == 0 { + return false, E.New("ipv4: tcp: NAT port space exhausted") } - ipHdr.SetSourceAddr(s.inet4NextAddress) - tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet4Address) - tcpHdr.SetDestinationPort(s.tcpPort) + rewriteIPv4TCP(ipHdr, tcpHdr, s.txChecksumOffload, + s.inet4NextAddress, natPort, true, + s.inet4Address, s.tcpPort, true) } } - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( - header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), - ))) - } else { - tcpHdr.SetChecksum(0) - } - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) return true, nil } -func (s *System) resetIPv4TCP(origIPHdr header.IPv4, origTCPHdr header.TCP) error { - frontHeadroom := s.frontHeadroom + PacketOffset - newPacket := buf.NewSize(frontHeadroom + header.IPv4MinimumSize + header.TCPMinimumSize) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv4MinimumSize+header.TCPMinimumSize) - ipHdr := header.IPv4(newPacket.Bytes()) - ipHdr.Encode(&header.IPv4Fields{ - TotalLength: uint16(newPacket.Len()), - Protocol: uint8(header.TCPProtocolNumber), - SrcAddr: origIPHdr.DestinationAddr(), - DstAddr: origIPHdr.SourceAddr(), - }) - tcpHdr := header.TCP(ipHdr.Payload()) - fields := header.TCPFields{ - SrcPort: origTCPHdr.DestinationPort(), - DstPort: origTCPHdr.SourcePort(), - DataOffset: header.TCPMinimumSize, - Flags: header.TCPFlagRst, - } - if origTCPHdr.Flags()&header.TCPFlagAck != 0 { - fields.SeqNum = origTCPHdr.AckNumber() - } else { - fields.Flags |= header.TCPFlagAck - ackNum := origTCPHdr.SequenceNumber() + uint32(len(origTCPHdr.Payload())) - if origTCPHdr.Flags()&header.TCPFlagSyn != 0 { - ackNum++ - } - if origTCPHdr.Flags()&header.TCPFlagFin != 0 { - ackNum++ - } - fields.AckNum = ackNum - } - tcpHdr.Encode(&fields) - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) - } - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, error) { source := netip.AddrPortFrom(ipHdr.SourceAddr(), tcpHdr.SourcePort()) destination := netip.AddrPortFrom(ipHdr.DestinationAddr(), tcpHdr.DestinationPort()) @@ -484,87 +486,107 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err if session == nil { return false, E.New("ipv6: tcp: session not found: ", destination.Port()) } - ipHdr.SetSourceAddr(session.Destination.Addr()) - tcpHdr.SetSourcePort(session.Destination.Port()) - ipHdr.SetDestinationAddr(session.Source.Addr()) - tcpHdr.SetDestinationPort(session.Source.Port()) + rewriteIPv6TCP(ipHdr, tcpHdr, s.txChecksumOffload, + session.Destination.Addr(), session.Destination.Port(), true, + session.Source.Addr(), session.Source.Port(), true) } else { var loopback bool for _, inet6LoopbackAddress := range s.inet6LoopbackAddress { if destination.Addr() == inet6LoopbackAddress { - ipHdr.SetDestinationAddr(ipHdr.SourceAddr()) - ipHdr.SetSourceAddr(inet6LoopbackAddress) + rewriteIPv6TCP(ipHdr, tcpHdr, s.txChecksumOffload, + inet6LoopbackAddress, 0, false, + source.Addr(), 0, false) loopback = true break } } if !loopback { - natPort, err := s.tcpNat.Lookup(source, destination, s.handler) - if err != nil { - if errors.Is(err, ErrDrop) { - return false, nil - } else { - return false, s.resetIPv6TCP(ipHdr, tcpHdr) - } + natPort := s.tcpNat.Lookup(source, destination) + if natPort == 0 { + return false, E.New("ipv6: tcp: NAT port space exhausted") } - ipHdr.SetSourceAddr(s.inet6NextAddress) - tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet6Address) - tcpHdr.SetDestinationPort(s.tcpPort6) + rewriteIPv6TCP(ipHdr, tcpHdr, s.txChecksumOffload, + s.inet6NextAddress, natPort, true, + s.inet6Address, s.tcpPort6, true) } } - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( - header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), - ))) - } else { - tcpHdr.SetChecksum(0) - } return true, nil } -func (s *System) resetIPv6TCP(origIPHdr header.IPv6, origTCPHdr header.TCP) error { - frontHeadroom := s.frontHeadroom + PacketOffset - newPacket := buf.NewSize(frontHeadroom + header.IPv6MinimumSize + header.TCPMinimumSize) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv6MinimumSize+header.TCPMinimumSize) - ipHdr := header.IPv6(newPacket.Bytes()) - ipHdr.Encode(&header.IPv6Fields{ - PayloadLength: uint16(header.TCPMinimumSize), - TransportProtocol: header.TCPProtocolNumber, - SrcAddr: origIPHdr.DestinationAddr(), - DstAddr: origIPHdr.SourceAddr(), - }) - tcpHdr := header.TCP(ipHdr.Payload()) - fields := header.TCPFields{ - SrcPort: origTCPHdr.DestinationPort(), - DstPort: origTCPHdr.SourcePort(), - DataOffset: header.TCPMinimumSize, - Flags: header.TCPFlagRst, - } - if origTCPHdr.Flags()&header.TCPFlagAck != 0 { - fields.SeqNum = origTCPHdr.AckNumber() +func rewriteIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP, txChecksumOffload bool, + newSource netip.Addr, newSourcePort uint16, rewriteSourcePort bool, + newDestination netip.Addr, newDestinationPort uint16, rewriteDestinationPort bool, +) { + oldSource := ipHdr.SourceAddress() + oldDestination := ipHdr.DestinationAddress() + newSourceAddr := tcpip.AddrFrom4(newSource.As4()) + newDestinationAddr := tcpip.AddrFrom4(newDestination.As4()) + if newSourceAddr != oldSource { + ipHdr.SetSourceAddressWithChecksumUpdate(newSourceAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSourceAddr, true) + } + } + if newDestinationAddr != oldDestination { + ipHdr.SetDestinationAddressWithChecksumUpdate(newDestinationAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestinationAddr, true) + } + } + if txChecksumOffload { + if rewriteSourcePort { + tcpHdr.SetSourcePort(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPort(newDestinationPort) + } + tcpHdr.SetChecksum(0) } else { - fields.Flags |= header.TCPFlagAck - ackNum := origTCPHdr.SequenceNumber() + uint32(len(origTCPHdr.Payload())) - if origTCPHdr.Flags()&header.TCPFlagSyn != 0 { - ackNum++ + if rewriteSourcePort { + tcpHdr.SetSourcePortWithChecksumUpdate(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPortWithChecksumUpdate(newDestinationPort) } - if origTCPHdr.Flags()&header.TCPFlagFin != 0 { - ackNum++ + } +} + +func rewriteIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP, txChecksumOffload bool, + newSource netip.Addr, newSourcePort uint16, rewriteSourcePort bool, + newDestination netip.Addr, newDestinationPort uint16, rewriteDestinationPort bool, +) { + oldSource := ipHdr.SourceAddress() + oldDestination := ipHdr.DestinationAddress() + newSourceAddr := tcpip.AddrFrom16(newSource.As16()) + newDestinationAddr := tcpip.AddrFrom16(newDestination.As16()) + if newSourceAddr != oldSource { + ipHdr.SetSourceAddress(newSourceAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSourceAddr, true) } - fields.AckNum = ackNum } - tcpHdr.Encode(&fields) - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) + if newDestinationAddr != oldDestination { + ipHdr.SetDestinationAddress(newDestinationAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestinationAddr, true) + } } - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv6Version) + if txChecksumOffload { + if rewriteSourcePort { + tcpHdr.SetSourcePort(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPort(newDestinationPort) + } + tcpHdr.SetChecksum(0) } else { - newPacket.Advance(-s.frontHeadroom) + if rewriteSourcePort { + tcpHdr.SetSourcePortWithChecksumUpdate(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPortWithChecksumUpdate(newDestinationPort) + } } - return common.Error(s.tun.Write(newPacket.Bytes())) } func (s *System) processIPv4UDP(ipHdr header.IPv4, udpHdr header.UDP) error { @@ -594,19 +616,6 @@ func (s *System) processIPv6UDP(ipHdr header.IPv6, udpHdr header.UDP) error { } func (s *System) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - _, pErr := s.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) - if pErr != nil { - if !errors.Is(pErr, ErrDrop) { - if source.IsIPv4() { - ipHdr := userData.(header.IPv4) - s.rejectIPv4WithICMP(ipHdr, header.ICMPv4PortUnreachable) - } else { - ipHdr := userData.(header.IPv6) - s.rejectIPv6WithICMP(ipHdr, header.ICMPv6PortUnreachable) - } - } - return false, nil, nil, nil - } var writer N.PacketWriter if source.IsIPv4() { packet := userData.(header.IPv4) @@ -640,29 +649,6 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { return false, nil } - sourceAddr := ipHdr.SourceAddr() - destinationAddr := ipHdr.DestinationAddr() - if destinationAddr != s.inet4Address { - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return s.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, - timeout, - ) - }) - if err != nil { - if errors.Is(err, ErrReset) { - return false, s.rejectIPv4WithICMP(ipHdr, header.ICMPv4HostUnreachable) - } else if errors.Is(err, ErrDrop) { - return false, nil - } - } - if action != nil { - return false, action.WritePacket(buf.As(ipHdr).ToOwned()) - } - } icmpHdr.SetType(header.ICMPv4EchoReply) sourceAddress := ipHdr.SourceAddr() ipHdr.SetSourceAddr(ipHdr.DestinationAddr()) @@ -672,70 +658,10 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool return true, nil } -func (s *System) rejectIPv4WithICMP(ipHdr header.IPv4, code header.ICMPv4Code) error { - frontHeadroom := s.frontHeadroom + PacketOffset - mtu := s.mtu - const maxIPData = header.IPv4MinimumProcessableDatagramSize - header.IPv4MinimumSize - if mtu > maxIPData { - mtu = maxIPData - } - available := mtu - header.ICMPv4MinimumSize - if available < len(ipHdr)+header.ICMPv4MinimumErrorPayloadSize { - return nil - } - payload := ipHdr - if len(payload) > available { - payload = payload[:available] - } - newPacket := buf.NewSize(frontHeadroom + header.IPv4MinimumSize + header.ICMPv4MinimumSize + len(payload)) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv4MinimumSize+header.ICMPv4MinimumSize+len(payload)) - newIPHdr := header.IPv4(newPacket.Bytes()) - newIPHdr.Encode(&header.IPv4Fields{ - TotalLength: uint16(newPacket.Len()), - Protocol: uint8(header.ICMPv4ProtocolNumber), - SrcAddr: ipHdr.DestinationAddr(), - DstAddr: ipHdr.SourceAddr(), - }) - newIPHdr.SetChecksum(^newIPHdr.CalculateChecksum()) - icmpHdr := header.ICMPv4(newIPHdr.Payload()) - icmpHdr.SetType(header.ICMPv4DstUnreachable) - icmpHdr.SetCode(code) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(ipHdr.Payload(), 0))) - copy(icmpHdr.Payload(), payload) - if PacketOffset > 0 { - newPacket.ExtendHeader(PacketOffset)[3] = syscall.AF_INET - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool, error) { if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { return false, nil } - sourceAddr := ipHdr.SourceAddr() - destinationAddr := ipHdr.DestinationAddr() - if destinationAddr != s.inet6Address { - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return s.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, - timeout, - ) - }) - if errors.Is(err, ErrReset) { - return false, s.rejectIPv6WithICMP(ipHdr, header.ICMPv6AddressUnreachable) - } else if errors.Is(err, ErrDrop) { - return false, nil - } - if action != nil { - return false, action.WritePacket(buf.As(ipHdr).ToOwned()) - } - } icmpHdr.SetType(header.ICMPv6EchoReply) sourceAddress := ipHdr.SourceAddr() ipHdr.SetSourceAddr(ipHdr.DestinationAddr()) @@ -748,50 +674,6 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool return true, nil } -func (s *System) rejectIPv6WithICMP(ipHdr header.IPv6, code header.ICMPv6Code) error { - frontHeadroom := s.frontHeadroom + PacketOffset - mtu := s.mtu - const maxIPv6Data = header.IPv6MinimumMTU - header.IPv6FixedHeaderSize - if mtu > maxIPv6Data { - mtu = maxIPv6Data - } - available := mtu - header.ICMPv6ErrorHeaderSize - if available < header.IPv6MinimumSize { - return nil - } - payload := ipHdr - if len(payload) > available { - payload = payload[:available] - } - newPacket := buf.NewSize(frontHeadroom + header.IPv6MinimumSize + header.ICMPv6DstUnreachableMinimumSize + len(payload)) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv6MinimumSize+header.ICMPv6DstUnreachableMinimumSize+len(payload)) - newIPHdr := header.IPv6(newPacket.Bytes()) - newIPHdr.Encode(&header.IPv6Fields{ - PayloadLength: uint16(header.ICMPv6DstUnreachableMinimumSize + len(payload)), - TransportProtocol: header.ICMPv6ProtocolNumber, - SrcAddr: ipHdr.DestinationAddr(), - DstAddr: ipHdr.SourceAddr(), - }) - icmpHdr := header.ICMPv6(newIPHdr.Payload()) - icmpHdr.SetType(header.ICMPv6DstUnreachable) - icmpHdr.SetCode(code) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], - Src: newIPHdr.SourceAddressSlice(), - Dst: newIPHdr.DestinationAddressSlice(), - PayloadCsum: checksum.Checksum(payload, 0), - PayloadLen: len(payload), - })) - copy(icmpHdr.Payload(), payload) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv6Version) - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - type systemUDPPacketWriter4 struct { tun Tun frontHeadroom int @@ -868,45 +750,65 @@ func (w *systemUDPPacketWriter6) WritePacket(buffer *buf.Buffer, destination M.S return common.Error(w.tun.Write(newPacket.Bytes())) } -type systemICMPDirectPacketWriter4 struct { - tun Tun +func newSystemWriteback(tunInterface Tun, frontHeadroom int) ForwardWriteback { + if linuxTUN, isLinuxTUN := tunInterface.(LinuxTUN); isLinuxTUN { + return &systemWritebackLinux{linuxTUN: linuxTUN, frontHeadroom: frontHeadroom} + } + if darwinTUN, isDarwinTUN := tunInterface.(DarwinTUN); isDarwinTUN { + return &systemWritebackDarwin{darwinTUN: darwinTUN, frontHeadroom: frontHeadroom} + } + return &systemWriteback{tun: tunInterface, frontHeadroom: frontHeadroom} +} + +type systemWritebackLinux struct { + linuxTUN LinuxTUN frontHeadroom int - source netip.Addr } -func (w *systemICMPDirectPacketWriter4) WritePacket(p []byte) error { - newPacket := buf.NewSize(w.frontHeadroom + len(p)) - defer newPacket.Release() - newPacket.Resize(w.frontHeadroom, 0) - newPacket.Write(p) - ipHdr := header.IPv4(newPacket.Bytes()) - ipHdr.SetDestinationAddr(w.source) - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) - } else { - newPacket.Advance(-w.frontHeadroom) +func (w *systemWritebackLinux) ReturnHeadroom() int { + return w.frontHeadroom + PacketOffset +} + +func (w *systemWritebackLinux) WriteReturnPackets(packets [][]byte) error { + return common.Error(w.linuxTUN.BatchWrite(packets, w.frontHeadroom)) +} + +type systemWritebackDarwin struct { + darwinTUN DarwinTUN + frontHeadroom int +} + +func (w *systemWritebackDarwin) ReturnHeadroom() int { + return w.frontHeadroom + PacketOffset +} + +func (w *systemWritebackDarwin) WriteReturnPackets(packets [][]byte) error { + buffers := make([]*buf.Buffer, 0, len(packets)) + for _, packet := range packets { + buffers = append(buffers, buf.As(packet[PacketOffset:])) } - return common.Error(w.tun.Write(newPacket.Bytes())) + return w.darwinTUN.BatchWrite(buffers) } -type systemICMPDirectPacketWriter6 struct { +type systemWriteback struct { tun Tun frontHeadroom int - source netip.Addr } -func (w *systemICMPDirectPacketWriter6) WritePacket(p []byte) error { - newPacket := buf.NewSize(w.frontHeadroom + len(p)) - defer newPacket.Release() - newPacket.Resize(w.frontHeadroom, 0) - newPacket.Write(p) - ipHdr := header.IPv6(newPacket.Bytes()) - ipHdr.SetDestinationAddr(w.source) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv6Version) - } else { - newPacket.Advance(-w.frontHeadroom) +func (w *systemWriteback) ReturnHeadroom() int { + return w.frontHeadroom + PacketOffset +} + +func (w *systemWriteback) WriteReturnPackets(packets [][]byte) error { + var writeErrors []error + for _, packet := range packets { + if PacketOffset > 0 { + PacketFillHeader(packet, header.IPVersion(packet[PacketOffset:])) + } + _, err := w.tun.Write(packet) + if err != nil { + writeErrors = append(writeErrors, err) + } } - return common.Error(w.tun.Write(newPacket.Bytes())) + return E.Errors(writeErrors...) } diff --git a/stack_system_nat.go b/stack_system_nat.go index cc460171..2fec29c0 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -5,9 +5,6 @@ import ( "net/netip" "sync" "time" - - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" ) type TCPNat struct { @@ -57,18 +54,36 @@ func (n *TCPNat) loopCheckTimeout(ctx context.Context) { func (n *TCPNat) checkTimeout() { now := time.Now() - n.portAccess.Lock() - defer n.portAccess.Unlock() - n.addrAccess.Lock() - defer n.addrAccess.Unlock() + type expiredSession struct { + port uint16 + session *TCPSession + } + var expired []expiredSession + n.portAccess.RLock() for natPort, session := range n.portMap { session.Lock() - if now.Sub(session.LastActive) > n.timeout { - delete(n.addrMap, tcpNatKey{Source: session.Source, Destination: session.Destination}) - delete(n.portMap, natPort) - } + timedOut := now.Sub(session.LastActive) > n.timeout session.Unlock() + if timedOut { + expired = append(expired, expiredSession{port: natPort, session: session}) + } + } + n.portAccess.RUnlock() + if len(expired) == 0 { + return + } + n.addrAccess.Lock() + n.portAccess.Lock() + for _, e := range expired { + e.session.Lock() + if now.Sub(e.session.LastActive) > n.timeout { + delete(n.addrMap, tcpNatKey{Source: e.session.Source, Destination: e.session.Destination}) + delete(n.portMap, e.port) + } + e.session.Unlock() } + n.portAccess.Unlock() + n.addrAccess.Unlock() } func (n *TCPNat) LookupBack(port uint16) *TCPSession { @@ -85,34 +100,46 @@ func (n *TCPNat) LookupBack(port uint16) *TCPSession { return session } -func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort, handler Handler) (uint16, error) { +func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort) uint16 { key := tcpNatKey{Source: source, Destination: destination} n.addrAccess.RLock() port, loaded := n.addrMap[key] n.addrAccess.RUnlock() if loaded { - return port, nil - } - _, pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil, 0) - if pErr != nil { - return 0, pErr + return port } n.addrAccess.Lock() - nextPort := n.portIndex - if nextPort == 0 { - nextPort = 10000 - n.portIndex = 10001 - } else { - n.portIndex++ + defer n.addrAccess.Unlock() + if port, loaded = n.addrMap[key]; loaded { + return port } - n.addrMap[key] = nextPort - n.addrAccess.Unlock() n.portAccess.Lock() + defer n.portAccess.Unlock() + nextPort, ok := n.allocatePortLocked() + if !ok { + return 0 + } n.portMap[nextPort] = &TCPSession{ Source: source, Destination: destination, LastActive: time.Now(), } - n.portAccess.Unlock() - return nextPort, nil + n.addrMap[key] = nextPort + return nextPort +} + +func (n *TCPNat) allocatePortLocked() (uint16, bool) { + for range 65535 - 10000 + 1 { + nextPort := n.portIndex + if nextPort == 0 { + nextPort = 10000 + n.portIndex = 10001 + } else { + n.portIndex++ + } + if _, occupied := n.portMap[nextPort]; !occupied { + return nextPort, true + } + } + return 0, false } diff --git a/stack_system_packet.go b/stack_system_packet.go index 34fe51e4..a8f8076e 100644 --- a/stack_system_packet.go +++ b/stack_system_packet.go @@ -4,7 +4,7 @@ import ( "net/netip" "syscall" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" ) diff --git a/tun.go b/tun.go index 35cd0956..c6518f40 100644 --- a/tun.go +++ b/tun.go @@ -7,33 +7,23 @@ import ( "runtime" "strconv" "strings" - "time" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "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/common/ranges" ) type Handler interface { - PrepareConnection( - network string, - source M.Socksaddr, - destination M.Socksaddr, - routeContext DirectRouteContext, - timeout time.Duration, - ) (DirectRouteDestination, error) + JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) FlowVerdict N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } -type DirectRouteContext interface { - WritePacket(packet []byte) error -} - type Tun interface { io.ReadWriter Name() (string, error) @@ -68,6 +58,12 @@ const ( DefaultIPRoute2AutoRedirectFallbackRuleIndex = 32768 ) +const ( + DNSModeDisabled = "disabled" + DNSModeNative = "native" + DNSModeHijack = "hijack" +) + type Options struct { Name string Inet4Address []netip.Prefix @@ -78,7 +74,8 @@ type Options struct { InterfaceScope bool Inet4Gateway netip.Addr Inet6Gateway netip.Addr - DNSServers []netip.Addr + DNSMode string + DNSAddress []netip.Addr IPRoute2TableIndex int IPRoute2RuleIndex int IPRoute2AutoRedirectFallbackRuleIndex int @@ -102,6 +99,8 @@ type Options struct { IncludeAndroidUser []int IncludePackage []string ExcludePackage []string + IncludeMACAddress []net.HardwareAddr + ExcludeMACAddress []net.HardwareAddr InterfaceFinder control.InterfaceFinder InterfaceMonitor DefaultInterfaceMonitor FileDescriptor int @@ -122,6 +121,57 @@ type Options struct { EXP_SendMsgX bool } +func (o *Options) DNSModeOrDefault() string { + if o.DNSMode == "" { + return DNSModeHijack + } + return o.DNSMode +} + +func (o *Options) DNSServerAddress() ([]netip.Addr, error) { + inet4DNS, err := o.Inet4DNSAddress() + if err != nil { + return nil, err + } + inet6DNS, err := o.Inet6DNSAddress() + if err != nil { + return nil, err + } + return append(inet4DNS, inet6DNS...), nil +} + +func (o *Options) Inet4DNSAddress() ([]netip.Addr, error) { + if len(o.Inet4Address) == 0 { + return nil, nil + } + if len(o.DNSAddress) > 0 { + return common.Filter(o.DNSAddress, netip.Addr.Is4), nil + } + if HasNextAddress(o.Inet4Address[0], 1) { + return []netip.Addr{o.Inet4Address[0].Addr().Next()}, nil + } + if !(len(o.Inet6Address) > 0 && HasNextAddress(o.Inet6Address[0], 1)) { + return nil, E.New("no IPv4 server configured and no usable next address in ", o.Inet6Address[0], " for DNS") + } + return nil, nil +} + +func (o *Options) Inet6DNSAddress() ([]netip.Addr, error) { + if len(o.Inet6Address) == 0 { + return nil, nil + } + if len(o.DNSAddress) > 0 { + return common.Filter(o.DNSAddress, netip.Addr.Is6), nil + } + if HasNextAddress(o.Inet6Address[0], 1) { + return []netip.Addr{o.Inet6Address[0].Addr().Next()}, nil + } + if !(len(o.Inet4Address) > 0 && HasNextAddress(o.Inet4Address[0], 1)) { + return nil, E.New("no IPv6 server configured and no usable next address in ", o.Inet6Address[0], " for DNS") + } + return nil, nil +} + func (o *Options) Inet4GatewayAddr() netip.Addr { if o.Inet4Gateway.IsValid() { return o.Inet4Gateway @@ -175,7 +225,7 @@ func (o *Options) Inet6GatewayAddr() netip.Addr { } func CalculateInterfaceName(name string) (tunName string) { - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { tunName = "utun" } else if name != "" { tunName = name diff --git a/tun_darwin.go b/tun_darwin.go index 8aa6923f..4b00ac59 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -6,11 +6,12 @@ import ( "net" "net/netip" "os" + "sync" "syscall" "unsafe" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing-tun/internal/rawfile_darwin" + "github.com/sagernet/sing-tun/gtcpip/header" + rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing-tun/internal/stopfd_darwin" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -36,6 +37,8 @@ type NativeTun struct { msgHdrsOutput []rawfile.MsgHdrX buffers []*buf.Buffer stopFd stopfd.StopFD + readPoller *rawfile.Poller + writeAccess sync.Mutex options Options inet4Address [4]byte inet6Address [16]byte @@ -132,7 +135,8 @@ func New(options Options) (Tun, error) { stopFd: common.Must1(stopfd.New()), sendMsgX: options.EXP_SendMsgX, } - for i := 0; i < batchSize; i++ { + nativeTun.readPoller = common.Must1(rawfile.NewPoller(nativeTun.stopFd.ReadFD, tunFd)) + for i := range batchSize { nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU)) nativeTun.iovecsOutput[i] = newIovecBuffer(int(options.MTU)) } @@ -155,15 +159,26 @@ func (t *NativeTun) Start() error { func (t *NativeTun) Close() error { if t.options.EXP_ExternalConfiguration { - return t.tunFile.Close() + t.stopFd.Stop() + err := t.tunFile.Close() + t.closePollers() + t.stopFd.Close() + return err } defer flushDNSCache() t.stopFd.Stop() err := E.Errors(t.unsetRoutes(), t.tunFile.Close()) + t.closePollers() t.stopFd.Close() return err } +func (t *NativeTun) closePollers() { + if t.readPoller != nil { + _ = t.readPoller.Close() + } +} + func (t *NativeTun) Read(p []byte) (n int, err error) { return t.tunFile.Read(p) } @@ -350,9 +365,9 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { t.msgHdrs[i].Msg.Iov = &iovecs[0] t.msgHdrs[i].Msg.Iovlen = 2 } - n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.stopFd.ReadFD, t.tunFd, t.msgHdrs) + n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.readPoller, t.tunFd, t.msgHdrs) if errno != 0 { - for k := 0; k < n; k++ { + for k := range n { t.iovecs[k].buffer.Release() t.iovecs[k].buffer = nil } @@ -366,7 +381,7 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { return nil, nil } buffers := t.buffers - for k := 0; k < n; k++ { + for k := range n { buffer := t.iovecs[k].buffer t.iovecs[k].buffer = nil buffer.Truncate(int(t.msgHdrs[k].DataLen) - PacketOffset) @@ -377,6 +392,23 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { } func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { + t.writeAccess.Lock() + defer t.writeAccess.Unlock() + for len(buffers) > 0 { + chunk := buffers + if len(chunk) > t.batchSize { + chunk = chunk[:t.batchSize] + } + buffers = buffers[len(chunk):] + err := t.batchWriteChunk(chunk) + if err != nil { + return err + } + } + return nil +} + +func (t *NativeTun) batchWriteChunk(buffers []*buf.Buffer) error { if !t.sendMsgX { for i, buffer := range buffers { t.iovecsOutput[i].nextIovecsOutput(buffer) diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index ef940c3d..b506db40 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -43,12 +43,13 @@ func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { ep, err := fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - RXChecksumOffload: true, - PacketDispatchMode: fdbased.RecvMMsg, - MultiPendingPackets: t.options.EXP_MultiPendingPackets, - SendMsgX: t.options.EXP_SendMsgX, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + ProcessorsPerChannel: 1, + RXChecksumOffload: true, + PacketDispatchMode: fdbased.RecvMMsg, + MultiPendingPackets: t.options.EXP_MultiPendingPackets, + SendMsgX: t.options.EXP_SendMsgX, }) if err != nil { return nil, stack.NICOptions{}, err diff --git a/tun_linux.go b/tun_linux.go index 20fdce23..97dc6456 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -14,8 +14,8 @@ import ( "unsafe" "github.com/sagernet/netlink" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -39,10 +39,12 @@ type NativeTun struct { writeAccess sync.Mutex vnetHdr bool writeBuffer []byte + readRawConn syscall.RawConn + pendingBuffer []byte + pendingLength int vnetHdrWriteBuf []byte gsoToWrite []int tcpGROTable *tcpGROTable - udpGroAccess sync.Mutex udpGROTable *udpGROTable gro groDisablementFlags txChecksumOffload bool @@ -74,6 +76,14 @@ func New(options Options) (Tun, error) { tunFile: os.NewFile(uintptr(options.FileDescriptor), "tun"), options: options, } + if options.GSO { + err := nativeTun.enableGSO() + if err != nil { + if options.Logger != nil { + options.Logger.Warn(err) + } + } + } } return nativeTun, nil } @@ -187,23 +197,42 @@ func (t *NativeTun) enableGSO() error { if !vnetHdrEnabled { return E.Cause(err, "enable offload: IFF_VNET_HDR not enabled") } - err = setTCPOffload(t.tunFd) - if err != nil { - return E.Cause(err, "enable TCP offload") - } t.vnetHdr = true - t.writeBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) + t.writeBuffer = make([]byte, virtioNetHdrLen+gsoMaxSize) + t.pendingBuffer = make([]byte, virtioNetHdrLen+gsoMaxSize) t.tcpGROTable = newTCPGROTable() t.udpGROTable = newUDPGROTable() - err = setUDPOffload(t.tunFd) + err = setTCPOffload(t.tunFd) if err != nil { - t.gro.disableUDPGRO() + if !(errors.Is(err, unix.EPERM) && t.options.FileDescriptor != 0) { + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "enable offload: set tcp offload")) + } + t.gro.disableTCPGRO() + t.gro.disableUDPGRO() + } + } else { + err = setUDPOffload(t.tunFd) + if err != nil { + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "enable offload: set udp offload")) + } + t.gro.disableUDPGRO() + } + } + t.readRawConn, err = t.tunFile.SyscallConn() + if err != nil { + return E.Cause(err, "enable offload: get raw conn") } return nil } func (t *NativeTun) probeTCPGRO() error { - ipPort := netip.AddrPortFrom(t.options.Inet4Address[0].Addr(), 0) + probeAddr := netip.AddrFrom4([4]byte{127, 0, 0, 1}) + if len(t.options.Inet4Address) > 0 { + probeAddr = t.options.Inet4Address[0].Addr() + } + ipPort := netip.AddrPortFrom(probeAddr, 0) fingerprint := []byte("sing-tun-probe-tun-gro") segmentSize := len(fingerprint) iphLen := 20 @@ -260,6 +289,16 @@ func (t *NativeTun) Name() (string, error) { } func (t *NativeTun) Start() error { + if t.vnetHdr && t.gro.canTCPGRO() { + err := t.probeTCPGRO() + if err != nil { + t.gro.disableTCPGRO() + t.gro.disableUDPGRO() + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) + } + } + } if t.options.FileDescriptor != 0 { return nil } @@ -276,17 +315,6 @@ func (t *NativeTun) Start() error { return E.Cause(err, "set tun up") } - if t.vnetHdr && len(t.options.Inet4Address) > 0 { - err = t.probeTCPGRO() - if err != nil { - t.gro.disableTCPGRO() - t.gro.disableUDPGRO() - if t.options.Logger != nil { - t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) - } - } - } - if t.options.EXP_ExternalConfiguration { return nil } @@ -317,7 +345,12 @@ func (t *NativeTun) Start() error { return E.Cause(err, "set rules") } - t.setSearchDomainForSystemdResolved() + if t.options.DNSMode != DNSModeDisabled { + err = t.setSearchDomainForSystemdResolved() + if err != nil { + return E.Cause(err, "set search domain") + } + } if t.options.AutoRoute && runtime.GOOS == "android" { t.interfaceCallback = t.options.InterfaceMonitor.RegisterCallback(t.routeUpdate) @@ -332,7 +365,9 @@ func (t *NativeTun) Close() error { if t.options.EXP_ExternalConfiguration { return common.Close(common.PtrOrNil(t.tunFile)) } - t.unsetSearchDomainForSystemdResolved() + if t.options.DNSMode != DNSModeDisabled { + t.unsetSearchDomainForSystemdResolved() + } t.unsetAddresses() return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) } @@ -365,16 +400,24 @@ func (t *NativeTun) Read(p []byte) (n int, err error) { // each buffer. It mutates sizes to reflect the size of each element of bufs, // and returns the number of packets read. func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, error) { + payload, options, err := parseVirtioRead(in) + if err != nil { + return 0, err + } + return GSOSplit(payload, options, bufs, sizes, offset) +} + +func parseVirtioRead(in []byte) ([]byte, GSOOptions, error) { var hdr virtioNetHdr err := hdr.decode(in) if err != nil { - return 0, err + return nil, GSOOptions{}, err } in = in[virtioNetHdrLen:] options, err := hdr.toGSOOptions() if err != nil { - return 0, err + return nil, GSOOptions{}, err } // Don't trust HdrLen from the kernel as it can be equal to the length @@ -385,18 +428,26 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e options.HdrLen = options.CsumStart + 8 } else if options.GSOType != GSONone { if len(in) <= int(options.CsumStart+12) { - return 0, errors.New("packet is too short") + return nil, GSOOptions{}, errors.New("packet is too short") } tcpHLen := uint16(in[options.CsumStart+12] >> 4 * 4) if tcpHLen < 20 || tcpHLen > 60 { // A TCP header must be between 20 and 60 bytes in length. - return 0, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) + return nil, GSOOptions{}, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) } options.HdrLen = options.CsumStart + tcpHLen } - return GSOSplit(in, options, bufs, sizes, offset) + return in, options, nil +} + +func gsoSegmentCount(payload []byte, options GSOOptions) int { + dataLength := len(payload) - int(options.HdrLen) + if options.GSOType == GSONone || options.GSOSize == 0 || dataLength < int(options.GSOSize) { + return 1 + } + return (dataLength + int(options.GSOSize) - 1) / int(options.GSOSize) } func (t *NativeTun) Write(p []byte) (n int, err error) { @@ -431,21 +482,87 @@ func (t *NativeTun) BatchSize() int { return idealBatchSize } -func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (n int, err error) { +func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (int, error) { t.readAccess.Lock() defer t.readAccess.Unlock() - n, err = t.tunFile.Read(t.writeBuffer) - if err != nil { - return + var used int + if t.pendingLength > 0 { + pendingLength := t.pendingLength + t.pendingLength = 0 + count, err := handleVirtioRead(t.pendingBuffer[:pendingLength], buffers, readN, offset) + if err != nil { + return count, err + } + used = count + } + for used < len(buffers) { + var ( + readLength int + err error + ) + if used == 0 { + readLength, err = t.tunFile.Read(t.writeBuffer) + if err != nil { + return 0, err + } + } else { + readLength, err = t.readNonblocking(t.writeBuffer) + if err != nil || readLength == 0 { + break + } + } + payload, options, parseErr := parseVirtioRead(t.writeBuffer[:readLength]) + if parseErr != nil { + if used > 0 { + break + } + return 0, parseErr + } + if used > 0 && gsoSegmentCount(payload, options) > len(buffers)-used { + t.writeBuffer, t.pendingBuffer = t.pendingBuffer, t.writeBuffer + t.pendingLength = readLength + break + } + count, splitErr := GSOSplit(payload, options, buffers[used:], readN[used:], offset) + if splitErr != nil { + if used > 0 { + break + } + return count, splitErr + } + used += count } - return handleVirtioRead(t.writeBuffer[:n], buffers, readN, offset) + return used, nil +} + +func (t *NativeTun) readNonblocking(buffer []byte) (int, error) { + var ( + readLength int + readErr error + ) + controlErr := t.readRawConn.Read(func(fd uintptr) bool { + readLength, readErr = syscall.Read(int(fd), buffer) + return true + }) + if controlErr != nil { + return 0, controlErr + } + if readErr != nil { + if readErr == syscall.EAGAIN { + return 0, nil + } + return 0, readErr + } + return readLength, nil } func (t *NativeTun) BatchWrite(buffers [][]byte, offset int) (int, error) { t.writeAccess.Lock() defer func() { - t.tcpGROTable.reset() - t.udpGROTable.reset() + if t.vnetHdr { + t.tcpGROTable.reset() + t.udpGROTable.reset() + } t.writeAccess.Unlock() }() var ( @@ -1073,37 +1190,24 @@ func (t *NativeTun) routeUpdate(_ *control.Interface, flags int) { } } -func (t *NativeTun) setSearchDomainForSystemdResolved() { - if t.options.EXP_DisableDNSHijack { - return - } +func (t *NativeTun) setSearchDomainForSystemdResolved() error { ctlPath, err := exec.LookPath("resolvectl") if err != nil { - return - } - dnsServer := t.options.DNSServers - if len(dnsServer) == 0 { - if len(t.options.Inet4Address) > 0 && HasNextAddress(t.options.Inet4Address[0], 1) { - dnsServer = append(dnsServer, t.options.Inet4Address[0].Addr().Next()) - } - if len(t.options.Inet6Address) > 0 && HasNextAddress(t.options.Inet6Address[0], 1) { - dnsServer = append(dnsServer, t.options.Inet6Address[0].Addr().Next()) - } + return nil } - if len(dnsServer) == 0 { - return + dnsAddress, err := t.options.DNSServerAddress() + if err != nil { + return err } go func() { _ = shell.Exec(ctlPath, "domain", t.options.Name, "~.").Run() _ = shell.Exec(ctlPath, "default-route", t.options.Name, "true").Run() - _ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Run() + _ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsAddress, netip.Addr.String)...)...).Run() }() + return nil } func (t *NativeTun) unsetSearchDomainForSystemdResolved() { - if t.options.EXP_DisableDNSHijack { - return - } ctlPath, err := exec.LookPath("resolvectl") if err != nil { return diff --git a/tun_linux_flags.go b/tun_linux_flags.go index 53fff08a..347dba0f 100644 --- a/tun_linux_flags.go +++ b/tun_linux_flags.go @@ -7,8 +7,6 @@ import ( "syscall" "unsafe" - E "github.com/sagernet/sing/common/exceptions" - "golang.org/x/sys/unix" ) @@ -33,13 +31,17 @@ func checkVNETHDREnabled(fd int, name string) (bool, error) { func setTCPOffload(fd int) error { err := unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, tunTCPOffloads) if err != nil { - return E.Cause(os.NewSyscallError("TUNSETOFFLOAD", err), "enable offload") + return os.NewSyscallError("TUNSETOFFLOAD", err) } return nil } func setUDPOffload(fd int) error { - return unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) + err := unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) + if err != nil { + return os.NewSyscallError("TUNSETOFFLOAD", err) + } + return nil } type ifreqData struct { diff --git a/tun_linux_gvisor.go b/tun_linux_gvisor.go index 8adf4c5d..ac4a76e7 100644 --- a/tun_linux_gvisor.go +++ b/tun_linux_gvisor.go @@ -74,12 +74,13 @@ func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { if t.vnetHdr { ep, err := fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - GSOMaxSize: gsoMaxSize, - GRO: true, - RXChecksumOffload: true, - TXChecksumOffload: t.txChecksumOffload, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + ProcessorsPerChannel: 1, + GSOMaxSize: gsoMaxSize, + GRO: true, + RXChecksumOffload: true, + TXChecksumOffload: t.txChecksumOffload, }) if err != nil { return nil, stack.NICOptions{}, err @@ -87,10 +88,11 @@ func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) return ep, stack.NICOptions{}, nil } else { ep, err := fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - RXChecksumOffload: true, - TXChecksumOffload: t.txChecksumOffload, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + ProcessorsPerChannel: 1, + RXChecksumOffload: true, + TXChecksumOffload: t.txChecksumOffload, }) if err != nil { return nil, stack.NICOptions{}, err diff --git a/tun_offload.go b/tun_offload.go index a0eee82f..f68fbfeb 100644 --- a/tun_offload.go +++ b/tun_offload.go @@ -4,14 +4,9 @@ import ( "encoding/binary" "fmt" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" -) - -const ( - gsoMaxSize = 65536 - idealBatchSize = 128 + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" ) // GSOType represents the type of segmentation offload. @@ -161,22 +156,20 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO } else { protocol = ipProtoUDP } + pseudoSumBase := header.PseudoHeaderChecksum(tcpip.TransportProtocolNumber(protocol), in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], 0) nextSegmentDataAt := int(options.HdrLen) i := 0 for ; nextSegmentDataAt < len(in); i++ { if i == len(outBufs) { return i - 1, ErrTooManySegments } - nextSegmentEnd := nextSegmentDataAt + int(options.GSOSize) - if nextSegmentEnd > len(in) { - nextSegmentEnd = len(in) - } + nextSegmentEnd := min(nextSegmentDataAt+int(options.GSOSize), len(in)) segmentDataLen := nextSegmentEnd - nextSegmentDataAt totalLen := int(options.HdrLen) + segmentDataLen sizes[i] = totalLen out := outBufs[i][outOffset:] - copy(out, in[:iphLen]) + copy(out[:options.HdrLen], in[:options.HdrLen]) if ipVersion == 4 { // For IPv4 we are responsible for incrementing the ID field, // updating the total len field, and recalculating the header @@ -195,9 +188,6 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO binary.BigEndian.PutUint16(out[4:], uint16(totalLen-iphLen)) } - // copy transport header - copy(out[options.CsumStart:options.HdrLen], in[options.CsumStart:options.HdrLen]) - if protocol == ipProtoTCP { // set TCP seq and adjust TCP flags tcpSeq := firstTCPSeqNum + uint32(options.GSOSize*uint16(i)) @@ -219,7 +209,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO out[transportCsumAt], out[transportCsumAt+1] = 0, 0 // clear tcp/udp checksum transportHeaderLen := int(options.HdrLen - options.CsumStart) lenForPseudo := uint16(transportHeaderLen + segmentDataLen) - transportCSum := header.PseudoHeaderChecksum(tcpip.TransportProtocolNumber(protocol), in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) + transportCSum := checksum.Combine(pseudoSumBase, lenForPseudo) transportCSum = ^checksum.Checksum(out[options.CsumStart:totalLen], transportCSum) binary.BigEndian.PutUint16(out[options.CsumStart+options.CsumOffset:], transportCSum) diff --git a/tun_offload_linux.go b/tun_offload_linux.go index 77337607..4e4dc79c 100644 --- a/tun_offload_linux.go +++ b/tun_offload_linux.go @@ -11,15 +11,21 @@ import ( "errors" "fmt" "io" + "slices" "unsafe" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" "golang.org/x/sys/unix" ) +const ( + gsoMaxSize = 65536 + idealBatchSize = 128 +) + // virtioNetHdr is defined in the kernel in include/uapi/linux/virtio_net.h. The // kernel symbol is virtio_net_hdr. type virtioNetHdr struct { @@ -123,14 +129,12 @@ func (t *tcpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, t if ok { return items, ok } - // TODO: insert() performs another map lookup. This could be rearranged to avoid. - t.insert(pkt, srcAddrOffset, dstAddrOffset, tcphOffset, tcphLen, bufsIndex) + t.insert(key, pkt, tcphOffset, tcphLen, bufsIndex) return nil, false } // insert an item in the table for the provided packet and packet metadata. -func (t *tcpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, tcphOffset, tcphLen, bufsIndex int) { - key := newTCPFlowKey(pkt, srcAddrOffset, dstAddrOffset, tcphOffset) +func (t *tcpGROTable) insert(key tcpFlowKey, pkt []byte, tcphOffset, tcphLen, bufsIndex int) { item := tcpGROItem{ key: key, bufsIndex: uint16(bufsIndex), @@ -230,14 +234,12 @@ func (u *udpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, u if ok { return items, ok } - // TODO: insert() performs another map lookup. This could be rearranged to avoid. - u.insert(pkt, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex, false) + u.insert(key, pkt, udphOffset, bufsIndex, false) return nil, false } // insert an item in the table for the provided packet and packet metadata. -func (u *udpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex int, cSumKnownInvalid bool) { - key := newUDPFlowKey(pkt, srcAddrOffset, dstAddrOffset, udphOffset) +func (u *udpGROTable) insert(key udpFlowKey, pkt []byte, udphOffset, bufsIndex int, cSumKnownInvalid bool) { item := udpGROItem{ key: key, bufsIndex: uint16(bufsIndex), @@ -450,7 +452,8 @@ func coalesceUDPPackets(pkt []byte, item *udpGROItem, bufs [][]byte, bufsOffset return coalescePktInvalidCSum } extendBy := len(pkt) - int(headersLen) - bufs[item.bufsIndex] = append(bufs[item.bufsIndex], make([]byte, extendBy)...) + b := bufs[item.bufsIndex] + bufs[item.bufsIndex] = b[:len(b)+extendBy] copy(bufs[item.bufsIndex][bufsOffset+len(pktHead):], pkt[headersLen:]) item.numMerged++ @@ -487,7 +490,8 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize } item.sentSeq = seq extendBy := coalescedLen - len(pktHead) - bufs[pktBuffsIndex] = append(bufs[pktBuffsIndex], make([]byte, extendBy)...) + b := bufs[pktBuffsIndex] + bufs[pktBuffsIndex] = b[:len(b)+extendBy] copy(bufs[pktBuffsIndex][bufsOffset+len(pkt):], bufs[item.bufsIndex][bufsOffset+int(headersLen):]) // Flip the slice headers in bufs as part of prepend. The index of item // is already being tracked for writing. @@ -513,7 +517,8 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize pktHead[item.iphLen+tcpFlagsOffset] |= tcpFlagPSH } extendBy := len(pkt) - int(headersLen) - bufs[item.bufsIndex] = append(bufs[item.bufsIndex], make([]byte, extendBy)...) + b := bufs[item.bufsIndex] + bufs[item.bufsIndex] = b[:len(b)+extendBy] copy(bufs[item.bufsIndex][bufsOffset+len(pktHead):], pkt[headersLen:]) } @@ -606,7 +611,7 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) if !existing { return groResultTableInsert } - for i := len(items) - 1; i >= 0; i-- { + for i, item := range slices.Backward(items) { // In the best case of packets arriving in order iterating in reverse is // more efficient if there are multiple items for a given flow. This // also enables a natural table.deleteAt() in the @@ -615,7 +620,6 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) // unordered packets, where pkt may land anywhere in items from a // sequence number perspective, however once an item is inserted into // the table it is never compared across other items later. - item := items[i] can := tcpPacketsCanCoalesce(pkt, uint8(iphLen), uint8(tcphLen), seq, pshSet, gsoSize, item, bufs, offset) if can != coalesceUnavailable { result := coalesceTCPPackets(can, pkt, pktI, gsoSize, seq, pshSet, &item, bufs, offset, isV6) @@ -634,7 +638,7 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) } } // failed to coalesce with any other packets; store the item in the flow - table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, tcphLen, pktI) + table.insert(newTCPFlowKey(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen), pkt, iphLen, tcphLen, pktI) return groResultTableInsert } @@ -793,7 +797,8 @@ func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType { if len(b) < 28 { return notGROCandidate } - if b[0]>>4 == 4 { + switch b[0] >> 4 { + case 4: if b[0]&0x0F != 5 { // IPv4 packets w/IP options do not coalesce return notGROCandidate @@ -804,7 +809,7 @@ func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType { if b[9] == unix.IPPROTO_UDP && gro.canUDPGRO() { return udp4GROCandidate } - } else if b[0]>>4 == 6 { + case 6: if b[6] == unix.IPPROTO_TCP && len(b) >= 60 && gro.canTCPGRO() { return tcp6GROCandidate } @@ -894,7 +899,7 @@ func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool) } } // failed to coalesce with any other packets; store the item in the flow - table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, pktI, pktCSumKnownInvalid) + table.insert(newUDPFlowKey(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen), pkt, iphLen, pktI, pktCSumKnownInvalid) return groResultTableInsert } diff --git a/tun_windows.go b/tun_windows.go index d00d51db..6dfce2f2 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -16,7 +16,6 @@ import ( "github.com/sagernet/sing-tun/internal/winipcfg" "github.com/sagernet/sing-tun/internal/winsys" "github.com/sagernet/sing-tun/internal/wintun" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/windnsapi" @@ -81,16 +80,14 @@ func (t *NativeTun) configure() error { if err != nil { return E.Cause(err, "set ipv4 address") } - if t.options.AutoRoute && !t.options.EXP_DisableDNSHijack { - dnsServers := common.Filter(t.options.DNSServers, netip.Addr.Is4) - if len(dnsServers) == 0 && HasNextAddress(t.options.Inet4Address[0], 1) { - dnsServers = []netip.Addr{t.options.Inet4Address[0].Addr().Next()} + if t.options.AutoRoute && t.options.DNSModeOrDefault() != DNSModeDisabled { + dnsServers, err := t.options.Inet4DNSAddress() + if err != nil { + return err } - if len(dnsServers) > 0 { - err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), dnsServers, nil) - if err != nil { - return E.Cause(err, "set ipv4 dns") - } + err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), dnsServers, nil) + if err != nil { + return E.Cause(err, "set ipv4 dns") } } else { err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), nil, nil) @@ -104,16 +101,14 @@ func (t *NativeTun) configure() error { if err != nil { return E.Cause(err, "set ipv6 address") } - if t.options.AutoRoute && !t.options.EXP_DisableDNSHijack { - dnsServers := common.Filter(t.options.DNSServers, netip.Addr.Is6) - if len(dnsServers) == 0 && HasNextAddress(t.options.Inet6Address[0], 1) { - dnsServers = []netip.Addr{t.options.Inet6Address[0].Addr().Next()} + if t.options.AutoRoute && t.options.DNSModeOrDefault() != DNSModeDisabled { + dnsServers, err := t.options.Inet6DNSAddress() + if err != nil { + return err } - if len(dnsServers) > 0 { - err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), dnsServers, nil) - if err != nil { - return E.Cause(err, "set ipv6 dns") - } + err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), dnsServers, nil) + if err != nil { + return E.Cause(err, "set ipv6 dns") } } else { err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), nil, nil) @@ -334,7 +329,7 @@ func (t *NativeTun) Start() error { } } - if !t.options.EXP_DisableDNSHijack { + if t.options.DNSModeOrDefault() == DNSModeHijack { blockDNSCondition := make([]winsys.FWPM_FILTER_CONDITION0, 1) blockDNSCondition[0].FieldKey = winsys.FWPM_CONDITION_IP_REMOTE_PORT blockDNSCondition[0].MatchType = winsys.FWP_MATCH_EQUAL