From 862174d21b51ec28e9cc3918b87379634bb3367f Mon Sep 17 00:00:00 2001 From: yaling888 <73897884+yaling888@users.noreply.github.com> Date: Thu, 30 Sep 2021 04:05:52 +0800 Subject: [PATCH] Feature: add lwIP TCP/IP stack to tun listener --- .github/workflows/go.yml | 10 ++- Makefile | 126 ++++++++++------------------ README.md | 4 +- config/config.go | 2 +- go.mod | 1 + go.sum | 2 + hub/executor/executor.go | 4 +- listener/tun/ipstack/commons/dns.go | 30 +++++++ listener/tun/ipstack/gvisor/tun.go | 1 + listener/tun/ipstack/lwip/dns.go | 88 +++++++++++++++++++ listener/tun/ipstack/lwip/tcp.go | 65 ++++++++++++++ listener/tun/ipstack/lwip/tun.go | 101 ++++++++++++++++++++++ listener/tun/ipstack/lwip/udp.go | 77 +++++++++++++++++ listener/tun/ipstack/system/dns.go | 37 ++------ listener/tun/ipstack/system/tcp.go | 7 +- listener/tun/tun_adapter.go | 5 +- 16 files changed, 443 insertions(+), 117 deletions(-) create mode 100644 listener/tun/ipstack/commons/dns.go create mode 100644 listener/tun/ipstack/lwip/dns.go create mode 100644 listener/tun/ipstack/lwip/tcp.go create mode 100644 listener/tun/ipstack/lwip/tun.go create mode 100644 listener/tun/ipstack/lwip/udp.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 401cd3de7..70aa6c9cd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -34,6 +34,10 @@ jobs: go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck -- $(go list ./...) + # init xgo + docker pull techknowlogick/xgo:latest + go install src.techknowlogick.com/xgo@latest + - name: SSH connection to Actions uses: P3TERX/ssh2actions@v1.0.0 if: github.actor == github.repository_owner && contains(github.event.head_commit.message, '[ssh]') @@ -43,15 +47,18 @@ jobs: env: NAME: clash BINDIR: bin - run: make -j releases + run: | + make cleancache && make -j releases - name: Prepare upload + if: startsWith(github.ref, 'refs/tags/') == false run: | echo "FILE_DATE=_$(date +"%Y%m%d%H%M")" >> $GITHUB_ENV echo "FILE_SHA=$(git describe --tags --always 2>/dev/null)" >> $GITHUB_ENV - name: Upload files to Artifacts uses: actions/upload-artifact@v2 + if: startsWith(github.ref, 'refs/tags/') == false with: name: clash_${{ env.FILE_SHA }}${{ env.FILE_DATE }} path: | @@ -76,5 +83,6 @@ jobs: with: keep_latest: 1 delete_tags: true + delete_tag_pattern: premium env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 413963ea1..e94c67a44 100644 --- a/Makefile +++ b/Makefile @@ -1,103 +1,61 @@ +GOCMD=go +XGOCMD=xgo -go go-1.17.x +GOBUILD=CGO_ENABLED=1 $(GOCMD) build -a -trimpath +GOCLEAN=$(GOCMD) clean NAME=clash -BINDIR=bin +BINDIR=$(shell pwd)/bin VERSION=$(shell git describe --tags --always 2>/dev/null || date +%F) BUILDTIME=$(shell date -u) -GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ - -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ - -w -s -buildid=' +BUILD_PACKAGE=. +RELEASE_LDFLAGS='-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ + -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ + -w -s -buildid=' +STATIC_LDFLAGS='-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ + -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ + -extldflags "-static" \ + -w -s -buildid=' PLATFORM_LIST = \ - darwin-amd64 \ - darwin-arm64 \ + darwin-10.12-amd64 \ + darwin-10.15-arm64 \ linux-386 \ linux-amd64 \ - linux-armv5 \ - linux-armv6 \ - linux-armv7 \ - linux-armv8 \ - linux-mips-softfloat \ - linux-mips-hardfloat \ - linux-mipsle-softfloat \ - linux-mipsle-hardfloat \ - linux-mips64 \ - linux-mips64le \ - freebsd-386 \ - freebsd-amd64 \ - freebsd-arm64 + linux-arm64 WINDOWS_ARCH_LIST = \ - windows-386 \ - windows-amd64 \ - windows-arm64 \ - windows-arm32v7 + windows-4.0-amd64 \ + windows-4.0-386 +# windows-arm64 -all: linux-amd64 darwin-amd64 windows-amd64 # Most used +all: linux-amd64 darwin-10.12-amd64 windows-4.0-amd64 # Most used -docker: - $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +build: + $(GOBUILD) -ldflags $(RELEASE_LDFLAGS) -o $(BINDIR)/$(NAME)-$@ -darwin-amd64: - GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +darwin-10.12-amd64: + $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=darwin-10.12/amd64 $(BUILD_PACKAGE) -darwin-arm64: - GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +darwin-10.15-arm64: + $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=darwin-10.15/arm64 $(BUILD_PACKAGE) linux-386: - GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(STATIC_LDFLAGS) -targets=linux/386 $(BUILD_PACKAGE) linux-amd64: - GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + GOARCH=amd64 GOOS=linux $(GOBUILD) -ldflags $(STATIC_LDFLAGS) -o $(BINDIR)/$(NAME)-$@ + #$(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(STATIC_LDFLAGS) -targets=linux/amd64 $(BUILD_PACKAGE) -linux-armv5: - GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +linux-arm64: + $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(STATIC_LDFLAGS) -targets=linux/arm64 $(BUILD_PACKAGE) -linux-armv6: - GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +windows-4.0-386: + $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=windows-4.0/386 $(BUILD_PACKAGE) -linux-armv7: - GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +windows-4.0-amd64: + $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=windows-4.0/amd64 $(BUILD_PACKAGE) -linux-armv8: - GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips-softfloat: - GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips-hardfloat: - GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mipsle-softfloat: - GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mipsle-hardfloat: - GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips64: - GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -linux-mips64le: - GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -freebsd-386: - GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -freebsd-amd64: - GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -freebsd-arm64: - GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ - -windows-386: - GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-amd64: - GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-arm64: - GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe - -windows-arm32v7: - GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe +#windows-arm64: +# $(XGOCMD) -dest=$(BINDIR) -out=$(NAME) -trimpath=true -ldflags=$(RELEASE_LDFLAGS) -targets=windows/arm64 $(BUILD_PACKAGE) gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) @@ -112,5 +70,13 @@ $(zip_releases): %.zip : % all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) releases: $(gz_releases) $(zip_releases) + clean: - rm $(BINDIR)/* \ No newline at end of file + rm -rf $(BINDIR) + mkdir -p $(BINDIR) + +cleancache: + # go build cache may need to cleanup if changing C source code + $(GOCLEAN) -cache + rm -rf $(BINDIR) + mkdir -p $(BINDIR) \ No newline at end of file diff --git a/README.md b/README.md index 663202f16..e7fe4a6fe 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,14 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash ### TUN configuration Supports macOS, Linux and Windows. +Support lwIP stack, a lightweight TCP/IP stack, recommend set to tun. + On Windows, you should download the [Wintun](https://www.wintun.net) driver and copy `wintun.dll` into Clash home directory. ```yaml # Enable the TUN listener tun: enable: true - stack: system # system or gvisor + stack: lwip # lwip(recommend), system or gvisor dns-listen: 0.0.0.0:53 # additional dns server listen on TUN auto-route: true # auto set global route ``` diff --git a/config/config.go b/config/config.go index 2b1bb2916..15baf6419 100644 --- a/config/config.go +++ b/config/config.go @@ -183,7 +183,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { ProxyGroup: []map[string]interface{}{}, Tun: Tun{ Enable: false, - Stack: "system", + Stack: "lwip", DNSListen: "0.0.0.0:53", AutoRoute: true, }, diff --git a/go.mod b/go.mod index 596ab397d..d34a4e745 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 + github.com/yaling888/go-lwip v0.0.0-20210928231210-94b50cb51cc1 go.uber.org/atomic v1.9.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f diff --git a/go.sum b/go.sum index df833f0d0..197f76c31 100644 --- a/go.sum +++ b/go.sum @@ -566,6 +566,8 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo= github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs= +github.com/yaling888/go-lwip v0.0.0-20210928231210-94b50cb51cc1 h1:bhAo5qI3SrfsNP0/91NJ5Rl4cqauE4CeNRplsIuRZTE= +github.com/yaling888/go-lwip v0.0.0-20210928231210-94b50cb51cc1/go.mod h1:Y+f95PkWh183q1oDJxdlxTHa2mpdHG5zvBhV0TUhhSY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 7a50b9402..25ad9bf36 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -151,8 +151,10 @@ func updateDNS(c *config.DNS, general *config.General) { resolver.DefaultResolver = r resolver.DefaultHostMapper = m - if general.Tun.Enable && strings.EqualFold(general.Tun.Stack, "system") { + if general.Tun.Enable && !strings.EqualFold(general.Tun.Stack, "gvisor") { resolver.DefaultLocalServer = dns.NewLocalServer(r, m) + } else { + resolver.DefaultLocalServer = nil } if err := dns.ReCreateServer(c.Listen, r, m); err != nil { diff --git a/listener/tun/ipstack/commons/dns.go b/listener/tun/ipstack/commons/dns.go new file mode 100644 index 000000000..4c4ca29b3 --- /dev/null +++ b/listener/tun/ipstack/commons/dns.go @@ -0,0 +1,30 @@ +package commons + +import ( + "github.com/Dreamacro/clash/component/resolver" + D "github.com/miekg/dns" +) + +func RelayDnsPacket(payload []byte) ([]byte, error) { + msg := &D.Msg{} + if err := msg.Unpack(payload); err != nil { + return nil, err + } + + r, err := resolver.ServeMsg(msg) + if err != nil { + return nil, err + } + + for _, ans := range r.Answer { + header := ans.Header() + + if header.Class == D.ClassINET && (header.Rrtype == D.TypeA || header.Rrtype == D.TypeAAAA) { + header.Ttl = 1 + } + } + + r.SetRcode(msg, r.Rcode) + r.Compress = true + return r.Pack() +} diff --git a/listener/tun/ipstack/gvisor/tun.go b/listener/tun/ipstack/gvisor/tun.go index d53f55b99..78e2b3f7f 100644 --- a/listener/tun/ipstack/gvisor/tun.go +++ b/listener/tun/ipstack/gvisor/tun.go @@ -195,6 +195,7 @@ func (t *gvisorAdapter) AsLinkEndpoint() (result stack.LinkEndpoint, err error) n, err := t.device.Read(packet) if err != nil && !t.device.IsClose() { log.Errorln("can not read from tun: %v", err) + continue } var p tcpip.NetworkProtocolNumber switch header.IPVersion(packet) { diff --git a/listener/tun/ipstack/lwip/dns.go b/listener/tun/ipstack/lwip/dns.go new file mode 100644 index 000000000..181909c46 --- /dev/null +++ b/listener/tun/ipstack/lwip/dns.go @@ -0,0 +1,88 @@ +package lwip + +import ( + "encoding/binary" + "io" + "net" + "time" + + "github.com/Dreamacro/clash/component/resolver" + D "github.com/Dreamacro/clash/listener/tun/ipstack/commons" + "github.com/Dreamacro/clash/log" + "github.com/yaling888/go-lwip" +) + +const defaultDnsReadTimeout = time.Second * 30 + +func shouldHijackDns(dnsIP net.IP, targetIp net.IP, targetPort int) bool { + if targetPort != 53 { + return false + } + + return dnsIP.Equal(net.IPv4zero) || dnsIP.Equal(targetIp) +} + +func hijackUDPDns(conn golwip.UDPConn, pkt []byte, addr *net.UDPAddr) { + go func() { + defer func(conn golwip.UDPConn) { + _ = conn.Close() + }(conn) + + answer, err := D.RelayDnsPacket(pkt) + if err != nil { + return + } + _, _ = conn.WriteFrom(answer, addr) + }() +} + +func hijackTCPDns(conn net.Conn) { + go func() { + defer func(conn net.Conn) { + _ = conn.Close() + }(conn) + + for { + if err := conn.SetDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil { + return + } + + var length uint16 + if binary.Read(conn, binary.BigEndian, &length) != nil { + return + } + + data := make([]byte, length) + + _, err := io.ReadFull(conn, data) + if err != nil { + return + } + + rb, err := D.RelayDnsPacket(data) + if err != nil { + continue + } + + if binary.Write(conn, binary.BigEndian, uint16(len(rb))) != nil { + return + } + + if _, err := conn.Write(rb); err != nil { + return + } + } + }() +} + +type dnsHandler struct { +} + +func NewDnsHandler() golwip.DnsHandler { + return &dnsHandler{} +} + +func (d dnsHandler) ResolveIP(host string) (net.IP, error) { + log.Debugln("[TUN] lwip resolve ip for host: %s", host) + return resolver.ResolveIP(host) +} diff --git a/listener/tun/ipstack/lwip/tcp.go b/listener/tun/ipstack/lwip/tcp.go new file mode 100644 index 000000000..0c668df6b --- /dev/null +++ b/listener/tun/ipstack/lwip/tcp.go @@ -0,0 +1,65 @@ +package lwip + +import ( + "net" + "strconv" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/log" + "github.com/yaling888/go-lwip" +) + +type tcpHandler struct { + dnsIP net.IP + tcpIn chan<- C.ConnContext +} + +func NewTCPHandler(dnsIP net.IP, tcpIn chan<- C.ConnContext) golwip.TCPConnHandler { + return &tcpHandler{dnsIP, tcpIn} +} + +func (h *tcpHandler) Handle(conn net.Conn, target *net.TCPAddr) error { + if shouldHijackDns(h.dnsIP, target.IP, target.Port) { + hijackTCPDns(conn) + + if log.Level() == log.DEBUG { + log.Debugln("[TUN] hijack dns tcp: %s:%d", target.IP.String(), target.Port) + } + return nil + } + + if conn.RemoteAddr() == nil { + _ = conn.Close() + return nil + } + + //if err := conn.SetDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil { + // _ = conn.Close() + // return nil + //} + + src, _ := conn.LocalAddr().(*net.TCPAddr) + dst, _ := conn.RemoteAddr().(*net.TCPAddr) + addrType := C.AtypIPv4 + if dst.IP.To4() == nil { + addrType = C.AtypIPv6 + } + + metadata := &C.Metadata{ + NetWork: C.TCP, + Type: C.TUN, + SrcIP: src.IP, + DstIP: dst.IP, + SrcPort: strconv.Itoa(src.Port), + DstPort: strconv.Itoa(dst.Port), + AddrType: addrType, + Host: "", + } + + go func(conn net.Conn, metadata *C.Metadata) { + h.tcpIn <- context.NewConnContext(conn, metadata) + }(conn, metadata) + + return nil +} diff --git a/listener/tun/ipstack/lwip/tun.go b/listener/tun/ipstack/lwip/tun.go new file mode 100644 index 000000000..ad3eafdcc --- /dev/null +++ b/listener/tun/ipstack/lwip/tun.go @@ -0,0 +1,101 @@ +package lwip + +import ( + "io" + "net" + "sync" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/config" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/tun/dev" + "github.com/Dreamacro/clash/listener/tun/ipstack" + "github.com/Dreamacro/clash/log" + "github.com/yaling888/go-lwip" +) + +type lwipAdapter struct { + device dev.TunDevice + lwipStack golwip.LWIPStack + lock sync.Mutex + mtu int + stackName string + dnsListen string + autoRoute bool +} + +func NewAdapter(device dev.TunDevice, conf config.Tun, mtu int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.TunAdapter, error) { + adapter := &lwipAdapter{ + device: device, + mtu: mtu, + stackName: conf.Stack, + dnsListen: conf.DNSListen, + autoRoute: conf.AutoRoute, + } + + adapter.lock.Lock() + defer adapter.lock.Unlock() + + //adapter.stopLocked() + + dnsHost, _, err := net.SplitHostPort(conf.DNSListen) + if err != nil { + return nil, err + } + + dnsIP := net.ParseIP(dnsHost) + + golwip.RegisterOutputFn(func(data []byte) (int, error) { + return device.Write(data) + }) + + // Setup TCP/IP stack. + lwipStack := golwip.NewLWIPStack(mtu) + adapter.lwipStack = lwipStack + + golwip.RegisterDnsHandler(NewDnsHandler()) + golwip.RegisterTCPConnHandler(NewTCPHandler(dnsIP, tcpIn)) + golwip.RegisterUDPConnHandler(NewUDPHandler(dnsIP, udpIn)) + + // Copy packets from tun device to lwip stack, it's the loop. + go func(lwipStack golwip.LWIPStack, device dev.TunDevice, mtu int) { + _, err := io.CopyBuffer(lwipStack.(io.Writer), device, make([]byte, mtu)) + if err != nil { + log.Errorln("copying data failed: %v", err) + } + }(lwipStack, device, mtu) + + return adapter, nil +} + +func (l *lwipAdapter) Stack() string { + return l.stackName +} + +func (l *lwipAdapter) AutoRoute() bool { + return l.autoRoute +} + +func (l *lwipAdapter) DNSListen() string { + return l.dnsListen +} + +func (l *lwipAdapter) Close() { + l.lock.Lock() + defer l.lock.Unlock() + + l.stopLocked() +} + +func (l *lwipAdapter) stopLocked() { + if l.lwipStack != nil { + l.lwipStack.Close() + } + + if l.device != nil { + _ = l.device.Close() + } + + l.lwipStack = nil + l.device = nil +} diff --git a/listener/tun/ipstack/lwip/udp.go b/listener/tun/ipstack/lwip/udp.go new file mode 100644 index 000000000..f3b3891d3 --- /dev/null +++ b/listener/tun/ipstack/lwip/udp.go @@ -0,0 +1,77 @@ +package lwip + +import ( + "io" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/socks5" + "github.com/yaling888/go-lwip" +) + +type udpPacket struct { + source *net.UDPAddr + payload []byte + sender golwip.UDPConn +} + +func (u *udpPacket) Data() []byte { + return u.payload +} + +func (u *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { + _, ok := addr.(*net.UDPAddr) + if !ok { + return 0, io.ErrClosedPipe + } + + return u.sender.WriteFrom(b, u.source) +} + +func (u *udpPacket) Drop() { +} + +func (u *udpPacket) LocalAddr() net.Addr { + return u.source +} + +type udpHandler struct { + dnsIP net.IP + udpIn chan<- *inbound.PacketAdapter +} + +func NewUDPHandler(dnsIP net.IP, udpIn chan<- *inbound.PacketAdapter) golwip.UDPConnHandler { + return &udpHandler{dnsIP, udpIn} +} + +func (h *udpHandler) Connect(conn golwip.UDPConn, target *net.UDPAddr) error { + return nil +} + +func (h *udpHandler) ReceiveTo(conn golwip.UDPConn, data []byte, addr *net.UDPAddr) error { + if shouldHijackDns(h.dnsIP, addr.IP, addr.Port) { + hijackUDPDns(conn, data, addr) + + if log.Level() == log.DEBUG { + log.Debugln("[TUN] hijack dns udp: %s:%d", addr.IP.String(), addr.Port) + } + return nil + } + + packet := &udpPacket{ + source: conn.LocalAddr(), + payload: data, + sender: conn, + } + + go func(addr *net.UDPAddr, packet *udpPacket) { + select { + case h.udpIn <- inbound.NewPacket(socks5.ParseAddrToSocksAddr(addr), packet, C.TUN): + default: + } + }(addr, packet) + + return nil +} diff --git a/listener/tun/ipstack/system/dns.go b/listener/tun/ipstack/system/dns.go index 21a223930..66ae340cd 100644 --- a/listener/tun/ipstack/system/dns.go +++ b/listener/tun/ipstack/system/dns.go @@ -6,10 +6,7 @@ import ( "net" "time" - "github.com/Dreamacro/clash/component/resolver" - - D "github.com/miekg/dns" - + D "github.com/Dreamacro/clash/listener/tun/ipstack/commons" "github.com/kr328/tun2socket/binding" "github.com/kr328/tun2socket/redirect" ) @@ -26,7 +23,7 @@ func shouldHijackDns(dnsAddr binding.Address, targetAddr binding.Address) bool { func hijackUDPDns(pkt []byte, ep *binding.Endpoint, sender redirect.UDPSender) { go func() { - answer, err := relayDnsPacket(pkt) + answer, err := D.RelayDnsPacket(pkt) if err != nil { return @@ -41,7 +38,9 @@ func hijackUDPDns(pkt []byte, ep *binding.Endpoint, sender redirect.UDPSender) { func hijackTCPDns(conn net.Conn) { go func() { - defer conn.Close() + defer func(conn net.Conn) { + _ = conn.Close() + }(conn) for { if err := conn.SetReadDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil { @@ -60,7 +59,7 @@ func hijackTCPDns(conn net.Conn) { return } - rb, err := relayDnsPacket(data) + rb, err := D.RelayDnsPacket(data) if err != nil { continue } @@ -75,27 +74,3 @@ func hijackTCPDns(conn net.Conn) { } }() } - -func relayDnsPacket(payload []byte) ([]byte, error) { - msg := &D.Msg{} - if err := msg.Unpack(payload); err != nil { - return nil, err - } - - r, err := resolver.ServeMsg(msg) - if err != nil { - return nil, err - } - - for _, ans := range r.Answer { - header := ans.Header() - - if header.Class == D.ClassINET && (header.Rrtype == D.TypeA || header.Rrtype == D.TypeAAAA) { - header.Ttl = 1 - } - } - - r.SetRcode(msg, r.Rcode) - r.Compress = true - return r.Pack() -} diff --git a/listener/tun/ipstack/system/tcp.go b/listener/tun/ipstack/system/tcp.go index 07b99eac8..18b9668d4 100644 --- a/listener/tun/ipstack/system/tcp.go +++ b/listener/tun/ipstack/system/tcp.go @@ -22,6 +22,11 @@ func handleTCP(conn net.Conn, endpoint *binding.Endpoint, tcpIn chan<- C.ConnCon Zone: "", } + addrType := C.AtypIPv4 + if dst.IP.To4() == nil { + addrType = C.AtypIPv6 + } + metadata := &C.Metadata{ NetWork: C.TCP, Type: C.TUN, @@ -29,7 +34,7 @@ func handleTCP(conn net.Conn, endpoint *binding.Endpoint, tcpIn chan<- C.ConnCon DstIP: dst.IP, SrcPort: strconv.Itoa(src.Port), DstPort: strconv.Itoa(dst.Port), - AddrType: C.AtypIPv4, + AddrType: addrType, Host: "", } diff --git a/listener/tun/tun_adapter.go b/listener/tun/tun_adapter.go index 643953283..1ef550801 100644 --- a/listener/tun/tun_adapter.go +++ b/listener/tun/tun_adapter.go @@ -11,6 +11,7 @@ import ( "github.com/Dreamacro/clash/listener/tun/dev" "github.com/Dreamacro/clash/listener/tun/ipstack" "github.com/Dreamacro/clash/listener/tun/ipstack/gvisor" + "github.com/Dreamacro/clash/listener/tun/ipstack/lwip" "github.com/Dreamacro/clash/listener/tun/ipstack/system" "github.com/Dreamacro/clash/log" ) @@ -33,7 +34,9 @@ func New(conf config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.Pack return nil, errors.New("unable to get device mtu") } - if strings.EqualFold(stack, "system") { + if strings.EqualFold(stack, "lwip") { + tunAdapter, err = lwip.NewAdapter(device, conf, mtu, tcpIn, udpIn) + } else if strings.EqualFold(stack, "system") { tunAdapter, err = system.NewAdapter(device, conf, mtu, tunAddress, tunAddress, func() {}, tcpIn, udpIn) } else if strings.EqualFold(stack, "gvisor") { tunAdapter, err = gvisor.NewAdapter(device, conf, tunAddress, tcpIn, udpIn)