commit 4f7247190c96f03ee13c3136f1d44d3fcbe483a7 Author: 世界 Date: Mon Jul 11 17:15:22 2022 +0800 Init commit diff --git a/.github/update_dependencies.sh b/.github/update_dependencies.sh new file mode 100755 index 0000000..d63123b --- /dev/null +++ b/.github/update_dependencies.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +PROJECTS=$(dirname "$0")/../.. + +go get -x github.com/sagernet/sing@$(git -C $PROJECTS/sing rev-parse HEAD) +go mod tidy diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml new file mode 100644 index 0000000..4b3e5d3 --- /dev/null +++ b/.github/workflows/debug.yml @@ -0,0 +1,40 @@ +name: Debug build + +on: + push: + branches: + - dev + paths-ignore: + - '**.md' + - '.github/**' + - '!.github/workflows/debug.yml' + pull_request: + branches: + - dev + +jobs: + build: + name: Debug build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Get latest go version + id: version + run: | + echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ steps.version.outputs.go_version }} + - name: Build + run: | + version=`git rev-parse HEAD` + mkdir build + pushd build + go mod init build + go get -v github.com/sagernet/sing-tun@$version + popd + go build -v ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7f8ac3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/vendor/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..5505a92 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,16 @@ +linters: + disable-all: true + enable: + - gofumpt + - govet + - gci + - staticcheck + +linters-settings: + gci: + sections: + - standard + - prefix(github.com/sagernet/sing) + - default + staticcheck: + go: '1.18' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3e3e29e --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..366eff6 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# sing-tun + +Simple transparent proxy library. + +Currently only for linux. + +## License + +``` +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +``` \ No newline at end of file diff --git a/format.go b/format.go new file mode 100644 index 0000000..e9ee0d9 --- /dev/null +++ b/format.go @@ -0,0 +1,7 @@ +package tun + +//go:generate go install -v mvdan.cc/gofumpt@latest +//go:generate go install -v github.com/daixiang0/gci@latest +//go:generate gofumpt -l -w . +//go:generate gofmt -s -w .k +//go:generate gci write -s "standard,prefix(github.com/sagernet/),default" . diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3591863 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/sagernet/sing-tun + +go 1.18 + +require ( + github.com/sagernet/sing v0.0.0-20220711062652-4394f7cbbae1 + github.com/vishvananda/netlink v1.1.0 + gvisor.dev/gvisor v0.0.0-20220711011657-cecae2f4234d +) + +require ( + github.com/google/btree v1.0.1 // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9ba509b --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/sagernet/sing v0.0.0-20220711062652-4394f7cbbae1 h1:gssTBQKiiXd1zALSOzQFZl3qwzCy4O76eSH0YY9A+Po= +github.com/sagernet/sing v0.0.0-20220711062652-4394f7cbbae1/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +gvisor.dev/gvisor v0.0.0-20220711011657-cecae2f4234d h1:KjI6i6P1ib9DiNdNIN8pb2TXfBewpKHf3O58cjj9vw4= +gvisor.dev/gvisor v0.0.0-20220711011657-cecae2f4234d/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= diff --git a/gvisor.go b/gvisor.go new file mode 100644 index 0000000..5d49ac7 --- /dev/null +++ b/gvisor.go @@ -0,0 +1,139 @@ +package tun + +import ( + "context" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +const defaultNIC tcpip.NICID = 1 + +type GVisorTun struct { + ctx context.Context + tunFd uintptr + tunMtu uint32 + handler Handler + stack *stack.Stack +} + +func NewGVisor(ctx context.Context, tunFd uintptr, tunMtu uint32, handler Handler) *GVisorTun { + return &GVisorTun{ + ctx: ctx, + tunFd: tunFd, + tunMtu: tunMtu, + handler: handler, + } +} + +func (t *GVisorTun) Start() error { + linkEndpoint, err := NewEndpoint(t.tunFd, t.tunMtu) + if err != nil { + return err + } + ipStack := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ + ipv4.NewProtocol, + ipv6.NewProtocol, + }, + TransportProtocols: []stack.TransportProtocolFactory{ + tcp.NewProtocol, + udp.NewProtocol, + icmp.NewProtocol4, + icmp.NewProtocol6, + }, + }) + tErr := ipStack.CreateNIC(defaultNIC, linkEndpoint) + if tErr != nil { + return E.New("create nic: ", tErr) + } + ipStack.SetRouteTable([]tcpip.Route{ + {Destination: header.IPv4EmptySubnet, NIC: defaultNIC}, + {Destination: header.IPv6EmptySubnet, NIC: defaultNIC}, + }) + ipStack.SetSpoofing(defaultNIC, true) + ipStack.SetPromiscuousMode(defaultNIC, true) + bufSize := 20 * 1024 + ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpip.TCPReceiveBufferSizeRangeOption{ + Min: 1, + Default: bufSize, + Max: bufSize, + }) + ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpip.TCPSendBufferSizeRangeOption{ + Min: 1, + Default: bufSize, + Max: bufSize, + }) + sOpt := tcpip.TCPSACKEnabled(true) + ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt) + mOpt := tcpip.TCPModerateReceiveBufferOption(true) + ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &mOpt) + tcpForwarder := tcp.NewForwarder(ipStack, 0, 1024, func(r *tcp.ForwarderRequest) { + var wq waiter.Queue + endpoint, err := r.CreateEndpoint(&wq) + if err != nil { + r.Complete(true) + return + } + r.Complete(false) + endpoint.SocketOptions().SetKeepAlive(true) + tcpConn := gonet.NewTCPConn(&wq, endpoint) + lAddr := tcpConn.RemoteAddr() + rAddr := tcpConn.LocalAddr() + if lAddr == nil || rAddr == nil { + tcpConn.Close() + return + } + go func() { + var metadata M.Metadata + metadata.Source = M.SocksaddrFromNet(lAddr) + metadata.Destination = M.SocksaddrFromNet(rAddr) + t.handler.NewConnection(t.ctx, tcpConn, metadata) + }() + }) + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, func(id stack.TransportEndpointID, buffer *stack.PacketBuffer) bool { + return tcpForwarder.HandlePacket(id, buffer) + }) + udpForwarder := udp.NewForwarder(ipStack, func(request *udp.ForwarderRequest) { + var wq waiter.Queue + endpoint, err := request.CreateEndpoint(&wq) + if err != nil { + return + } + udpConn := gonet.NewUDPConn(ipStack, &wq, endpoint) + lAddr := udpConn.RemoteAddr() + rAddr := udpConn.LocalAddr() + if lAddr == nil || rAddr == nil { + udpConn.Close() + return + } + go func() { + var metadata M.Metadata + metadata.Source = M.SocksaddrFromNet(lAddr) + metadata.Destination = M.SocksaddrFromNet(rAddr) + t.handler.NewPacketConnection(t.ctx, bufio.NewPacketConn(&bufio.UnbindPacketConn{ExtendedConn: bufio.NewExtendedConn(udpConn), Addr: M.SocksaddrFromNet(rAddr)}), metadata) + }() + }) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + t.stack = ipStack + return nil +} + +func (t *GVisorTun) Close() error { + return common.Close( + common.PtrOrNil(t.stack), + ) +} diff --git a/gvisor_linux.go b/gvisor_linux.go new file mode 100644 index 0000000..0cc35a6 --- /dev/null +++ b/gvisor_linux.go @@ -0,0 +1,22 @@ +package tun + +import ( + "runtime" + + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +func NewEndpoint(tunFd uintptr, tunMtu uint32) (stack.LinkEndpoint, error) { + var packetDispatchMode fdbased.PacketDispatchMode + if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" { + packetDispatchMode = fdbased.PacketMMap + } else { + packetDispatchMode = fdbased.RecvMMsg + } + return fdbased.New(&fdbased.Options{ + FDs: []int{int(tunFd)}, + MTU: tunMtu, + PacketDispatchMode: packetDispatchMode, + }) +} diff --git a/gvisor_nonlinux.go b/gvisor_nonlinux.go new file mode 100644 index 0000000..6fb5fd5 --- /dev/null +++ b/gvisor_nonlinux.go @@ -0,0 +1,9 @@ +//go:build !linux + +package tun + +import "gvisor.dev/gvisor/pkg/tcpip/stack" + +func NewEndpoint(tunFd uintptr, tunMtu uint32) (stack.LinkEndpoint, error) { + return NewPosixEndpoint(tunFd, tunMtu) +} diff --git a/gvisor_posix.go b/gvisor_posix.go new file mode 100644 index 0000000..1fda37b --- /dev/null +++ b/gvisor_posix.go @@ -0,0 +1,118 @@ +package tun + +import ( + "os" + + gBuffer "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/rw" +) + +var _ stack.LinkEndpoint = (*PosixEndpoint)(nil) + +type PosixEndpoint struct { + fd uintptr + mtu uint32 + file *os.File + dispatcher stack.NetworkDispatcher +} + +func NewPosixEndpoint(tunFd uintptr, tunMtu uint32) (stack.LinkEndpoint, error) { + return &PosixEndpoint{ + fd: tunFd, + mtu: tunMtu, + file: os.NewFile(tunFd, "tun"), + }, nil +} + +func (e *PosixEndpoint) MTU() uint32 { + return e.mtu +} + +func (e *PosixEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *PosixEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *PosixEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityNone +} + +func (e *PosixEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + if dispatcher == nil && e.dispatcher != nil { + e.dispatcher = nil + return + } + if dispatcher != nil && e.dispatcher == nil { + e.dispatcher = dispatcher + go e.dispatchLoop() + } +} + +func (e *PosixEndpoint) dispatchLoop() { + _buffer := buf.StackNewPacket() + defer common.KeepAlive(_buffer) + buffer := common.Dup(_buffer) + defer buffer.Release() + for { + n, err := e.file.Read(buffer.FreeBytes()) + if err != nil { + break + } + var view gBuffer.View + view.Append(buffer.To(n)) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: view, + IsForwardedPacket: true, + }) + defer pkt.DecRef() + var p tcpip.NetworkProtocolNumber + ipHeader, ok := pkt.Data().PullUp(1) + if !ok { + continue + } + switch header.IPVersion(ipHeader) { + case header.IPv4Version: + p = header.IPv4ProtocolNumber + case header.IPv6Version: + p = header.IPv6ProtocolNumber + default: + continue + } + e.dispatcher.DeliverNetworkPacket(p, pkt) + } +} + +func (e *PosixEndpoint) IsAttached() bool { + return e.dispatcher != nil +} + +func (e *PosixEndpoint) Wait() { +} + +func (e *PosixEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *PosixEndpoint) AddHeader(buffer *stack.PacketBuffer) { +} + +func (e *PosixEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) { + var n int + for _, packet := range pkts.AsSlice() { + _, err := rw.WriteV(e.fd, packet.Slices()) + if err != nil { + return n, &tcpip.ErrAborted{} + } + n++ + } + return n, nil +} diff --git a/tun.go b/tun.go new file mode 100644 index 0000000..4f0e006 --- /dev/null +++ b/tun.go @@ -0,0 +1,10 @@ +package tun + +import ( + N "github.com/sagernet/sing/common/network" +) + +type Handler interface { + N.TCPConnectionHandler + N.UDPConnectionHandler +} diff --git a/tun_linux.go b/tun_linux.go new file mode 100644 index 0000000..5c5c037 --- /dev/null +++ b/tun_linux.go @@ -0,0 +1,113 @@ +package tun + +import ( + "net" + "net/netip" + + "github.com/vishvananda/netlink" + "gvisor.dev/gvisor/pkg/tcpip/link/tun" +) + +func Open(name string) (uintptr, error) { + tunFd, err := tun.Open(name) + if err != nil { + return 0, err + } + return uintptr(tunFd), nil +} + +func Configure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, mtu uint32, autoRoute bool) error { + tunLink, err := netlink.LinkByName(name) + if err != nil { + return err + } + + if inet4Address.IsValid() { + addr4, _ := netlink.ParseAddr(inet4Address.String()) + err = netlink.AddrAdd(tunLink, addr4) + if err != nil { + return err + } + } + + if inet6Address.IsValid() { + addr6, _ := netlink.ParseAddr(inet6Address.String()) + err = netlink.AddrAdd(tunLink, addr6) + if err != nil { + return err + } + } + + err = netlink.LinkSetMTU(tunLink, int(mtu)) + if err != nil { + return err + } + + err = netlink.LinkSetUp(tunLink) + if err != nil { + return err + } + + if autoRoute { + if inet4Address.IsValid() { + err = netlink.RouteAdd(&netlink.Route{ + Dst: &net.IPNet{ + IP: net.IPv4zero, + Mask: net.CIDRMask(0, 32), + }, + LinkIndex: tunLink.Attrs().Index, + }) + if err != nil { + return err + } + } + if inet6Address.IsValid() { + err = netlink.RouteAdd(&netlink.Route{ + Dst: &net.IPNet{ + IP: net.IPv6zero, + Mask: net.CIDRMask(0, 128), + }, + LinkIndex: tunLink.Attrs().Index, + }) + if err != nil { + return err + } + } + } + + return nil +} + +func UnConfigure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, autoRoute bool) error { + if autoRoute { + tunLink, err := netlink.LinkByName(name) + if err != nil { + return err + } + if inet4Address.IsValid() { + err = netlink.RouteDel(&netlink.Route{ + Dst: &net.IPNet{ + IP: net.IPv4zero, + Mask: net.CIDRMask(0, 32), + }, + LinkIndex: tunLink.Attrs().Index, + }) + if err != nil { + return err + } + } + if inet6Address.IsValid() { + err = netlink.RouteDel(&netlink.Route{ + Dst: &net.IPNet{ + IP: net.IPv6zero, + Mask: net.CIDRMask(0, 128), + }, + LinkIndex: tunLink.Attrs().Index, + }) + if err != nil { + return err + } + } + } + return nil +} diff --git a/tun_other.go b/tun_other.go new file mode 100644 index 0000000..787c3c1 --- /dev/null +++ b/tun_other.go @@ -0,0 +1,20 @@ +//go:build !linux + +package tun + +import ( + "net/netip" + "os" +) + +func Open(name string) (uintptr, error) { + return 0, os.ErrInvalid +} + +func Configure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, mtu uint32, autoRoute bool) error { + return os.ErrInvalid +} + +func UnConfigure(name string, inet4Address netip.Prefix, inet6Address netip.Prefix, autoRoute bool) error { + return os.ErrInvalid +}