From 4f7247190c96f03ee13c3136f1d44d3fcbe483a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 Jul 2022 17:15:22 +0800 Subject: [PATCH] Init commit --- .github/update_dependencies.sh | 6 ++ .github/workflows/debug.yml | 40 ++++++++++ .gitignore | 2 + .golangci.yml | 16 ++++ LICENSE | 14 ++++ README.md | 24 ++++++ format.go | 7 ++ go.mod | 16 ++++ go.sum | 17 ++++ gvisor.go | 139 +++++++++++++++++++++++++++++++++ gvisor_linux.go | 22 ++++++ gvisor_nonlinux.go | 9 +++ gvisor_posix.go | 118 ++++++++++++++++++++++++++++ tun.go | 10 +++ tun_linux.go | 113 +++++++++++++++++++++++++++ tun_other.go | 20 +++++ 16 files changed, 573 insertions(+) create mode 100755 .github/update_dependencies.sh create mode 100644 .github/workflows/debug.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 format.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gvisor.go create mode 100644 gvisor_linux.go create mode 100644 gvisor_nonlinux.go create mode 100644 gvisor_posix.go create mode 100644 tun.go create mode 100644 tun_linux.go create mode 100644 tun_other.go 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 +}