diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df10585 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +.idea/ +.vscode/ +bin/ + +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..883092b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM golang:alpine AS builder + +WORKDIR /app +COPY . /app + +RUN apk add --no-cache make git \ + && go mod download \ + && make docker \ + && /app/bin/tun2socks-docker -v + +FROM alpine:latest + +COPY ./scripts/entrypoint.sh /entrypoint.sh +COPY --from=builder /app/bin/tun2socks-docker /usr/bin/tun2socks + +RUN apk add --update --no-cache iptables iproute2 \ + && chmod +x /entrypoint.sh + +ENV TUN tun0 +ENV ETH eth0 +ENV TUN_ADDR= +ENV TUN_MASK= +ENV LOGLEVEL= +ENV EXCLUDED= +ENV EXTRACMD= + +ENV API= +ENV DNS= +ENV HOSTS= +ENV PROXY= + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..502bb32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jason Lyu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..870861e --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +DIR = "bin" +NAME = "tun2socks" + +TAGS = "" +BUILD_FLAGS = "-v" + +VERSION = $(shell git describe --tags) +BUILD_TIME = $(shell date -u '+%FT%TZ') + +LDFLAGS += -w -s -buildid= +LDFLAGS += -X "main.Version=$(VERSION)" +LDFLAGS += -X "main.BuildTime=$(BUILD_TIME)" # RFC3339 +GO_BUILD = CGO_ENABLED=0 go build $(BUILD_FLAGS) -ldflags '$(LDFLAGS)' -tags '$(TAGS)' -trimpath + +PLATFORM_LIST = \ + darwin-amd64 \ + linux-amd64 \ + linux-arm64 \ + +.PHONY: $(PLATFORM_LIST) + +all: $(PLATFORM_LIST) + +docker: + $(GO_BUILD) -o $(DIR)/$(NAME)-$@ + +darwin-amd64: + GOARCH=amd64 GOOS=darwin $(GO_BUILD) -o $(DIR)/$(NAME)-$@ + +linux-amd64: + GOARCH=amd64 GOOS=linux $(GO_BUILD) -o $(DIR)/$(NAME)-$@ + +linux-arm64: + GOARCH=arm64 GOOS=linux $(GO_BUILD) -o $(DIR)/$(NAME)-$@ + +zip_releases=$(addsuffix .zip, $(PLATFORM_LIST)) + +$(zip_releases): %.zip : % + zip -m -j $(DIR)/$(NAME)-$(basename $@).zip $(DIR)/$(NAME)-$(basename $@) + +releases: $(zip_releases) + +clean: + rm $(DIR)/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8779d7 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# tun2socks + +A tun2socks implementation written in Go. diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 new file mode 100644 index 0000000..6728189 --- /dev/null +++ b/docker/Dockerfile.aarch64 @@ -0,0 +1,32 @@ +FROM arm64v8/golang:alpine AS builder + +WORKDIR /app +COPY . /app + +RUN apk add --no-cache make git \ + && go mod download \ + && make docker \ + && /app/bin/tun2socks-docker -v + +FROM arm64v8/alpine:latest + +COPY ./scripts/entrypoint.sh /entrypoint.sh +COPY --from=builder /app/bin/tun2socks-docker /usr/bin/tun2socks + +RUN apk add --update --no-cache iptables iproute2 \ + && chmod +x /entrypoint.sh + +ENV TUN tun0 +ENV ETH eth0 +ENV TUN_ADDR= +ENV TUN_MASK= +ENV LOGLEVEL= +ENV EXCLUDED= +ENV EXTRACMD= + +ENV API= +ENV DNS= +ENV HOSTS= +ENV PROXY= + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..6c0e950 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,35 @@ +version: '2.4' + +services: + tun2socks: + image: xjasonlyu/tun2socks:arm64-dev + cap_add: + - NET_ADMIN + devices: + - '/dev/net/tun:/dev/net/tun' + environment: + - GODEBUG=madvdontneed=1 + - PROXY= + - LOGLEVEL=warning + - API=:8080 + - DNS= + - HOSTS= + - EXCLUDED= + - EXTRACMD= + networks: + switch: + ipv4_address: 172.20.1.20 + restart: always + container_name: tun2socks + +networks: + switch: + name: switch + ipam: + driver: default + config: + - subnet: '172.20.1.0/25' + gateway: 172.20.1.1 + driver: macvlan + driver_opts: + parent: eth0 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aacf6d7 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/xjasonlyu/tun2socks + +go 1.15 + +require ( + github.com/Dreamacro/go-shadowsocks2 v0.1.6 + github.com/go-chi/chi v4.1.2+incompatible + github.com/go-chi/cors v1.1.1 + github.com/go-chi/render v1.0.1 + github.com/gofrs/uuid v3.3.0+incompatible + github.com/gorilla/websocket v1.4.2 + github.com/sirupsen/logrus v1.7.0 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + github.com/urfave/cli/v2 v2.2.0 + github.com/xjasonlyu/clash v0.15.1-0.20201105074459-aa45c8b56cf6 + go.uber.org/atomic v1.7.0 + golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect + golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 // indirect + golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect + gvisor.dev/gvisor v0.0.0-20201105065002-ab9a79fe812a +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..301a497 --- /dev/null +++ b/go.sum @@ -0,0 +1,453 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.52.1-0.20200122224058-0482b626c726/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Dreamacro/go-shadowsocks2 v0.1.6 h1:PysSf9sLT3Qn8jhlin5v7Rk68gOQG4K5BZFY1nxLGxI= +github.com/Dreamacro/go-shadowsocks2 v0.1.6/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= +github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups v0.0.0-20181219155423-39b18af02c41/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY= +github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/ttrpc v0.0.0-20200121165050-0be804eadb15/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20191028175130-9e7d5ac5ea55/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dpjacques/clockwork v0.1.1-0.20200827220843-c1f524b839be/go.mod h1:D8mP2A8vVT2GkXqPorSBmhnshhkFBYgzhA90KmJt25Y= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw= +github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v28 v28.1.2-0.20191108005307-e555eab49ce8/go.mod h1:g82e6OHbJ0WYrYeOrid1MMfHAtqjxBz+N74tfAt9KrQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= +github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs= +github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20181111125026-1722abf79c2f/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug= +github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng= +github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= +github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= +github.com/oschwald/maxminddb-golang v1.7.0 h1:JmU4Q1WBv5Q+2KZy5xJI+98aUwTIrPPxZUkd5Cwr8Zc= +github.com/oschwald/maxminddb-golang v1.7.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/xjasonlyu/clash v0.15.1-0.20201105074459-aa45c8b56cf6 h1:iQsLkjayjJs29VOeXaeznpy1jddiuRjstj6MfFbKVoM= +github.com/xjasonlyu/clash v0.15.1-0.20201105074459-aa45c8b56cf6/go.mod h1:eQ5eRTAjXlzRV4rB9pOujCwDl1mBF8htrMvKMqavqvo= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM= +golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM= +golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201021000207-d49c4edd7d96/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gvisor.dev/gvisor v0.0.0-20201105065002-ab9a79fe812a h1:io3nR9e7V26iORClxbTxnrNBhxAvdvgMKU8E+at6idc= +gvisor.dev/gvisor v0.0.0-20201105065002-ab9a79fe812a/go.mod h1:t4GUXJhnQEPtYSvrvRNW5CNdBQ2oWZBy7P+FbCVKBFY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs= +k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ= +k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ= +k8s.io/client-go v0.16.13/go.mod h1:UKvVT4cajC2iN7DCjLgT0KVY/cbY6DGdUCyRiIfws5M= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20200410163147-594e756bea31/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go new file mode 100644 index 0000000..e1fe0d6 --- /dev/null +++ b/internal/adapter/adapter.go @@ -0,0 +1,31 @@ +package adapter + +import "net" + +type TCPConn interface { + net.Conn + Metadata() *Metadata +} + +type UDPPacket interface { + // Data get the payload of UDP Packet. + Data() []byte + + // Drop call after packet is used, could release resources in this function. + Drop() + + // LocalAddr returns the source IP/Port of packet. + LocalAddr() net.Addr + + // Metadata returns the metadata of packet. + Metadata() *Metadata + + // RemoteAddr returns the destination IP/Port of packet. + RemoteAddr() net.Addr + + // WriteBack writes the payload with source IP/Port equals addr + // - variable source IP/Port is important to STUN + // - if addr is not provided, WriteBack will write out UDP packet with SourceIP/Port equals to original Target, + // this is important when using Fake-IP. + WriteBack([]byte, net.Addr) (int, error) +} diff --git a/internal/adapter/metadata.go b/internal/adapter/metadata.go new file mode 100644 index 0000000..b509e1f --- /dev/null +++ b/internal/adapter/metadata.go @@ -0,0 +1,96 @@ +package adapter + +import ( + "bytes" + "encoding/json" + "net" + "strconv" + + "github.com/xjasonlyu/clash/component/socks5" +) + +const ( + TCP Network = iota + UDP +) + +type Network int + +func (n Network) String() string { + if n == TCP { + return "tcp" + } + return "udp" +} + +func (n Network) MarshalJSON() ([]byte, error) { + return json.Marshal(n.String()) +} + +// Metadata implements the net.Addr interface. +type Metadata struct { + Net Network `json:"network"` + SrcIP net.IP `json:"sourceIP"` + MidIP net.IP `json:"dialerIP"` + DstIP net.IP `json:"destinationIP"` + SrcPort uint16 `json:"sourcePort"` + MidPort uint16 `json:"dialerPort"` + DstPort uint16 `json:"destinationPort"` + Host string `json:"host"` +} + +func (m *Metadata) DestinationAddress() string { + if m.Host != "" { + return net.JoinHostPort(m.Host, strconv.FormatUint(uint64(m.DstPort), 10)) + } else if m.DstIP != nil { + return net.JoinHostPort(m.DstIP.String(), strconv.FormatUint(uint64(m.DstPort), 10)) + } + return "" +} + +func (m *Metadata) SourceAddress() string { + return net.JoinHostPort(m.SrcIP.String(), strconv.FormatUint(uint64(m.SrcPort), 10)) +} + +func (m *Metadata) UDPAddr() *net.UDPAddr { + if m.Net != UDP || m.DstIP == nil { + return nil + } + return &net.UDPAddr{ + IP: m.DstIP, + Port: int(m.DstPort), + } +} + +func (m *Metadata) SerializesSocksAddr() socks5.Addr { + var buf [][]byte + port := []byte{uint8(m.DstPort >> 8), uint8(m.DstPort & 0xff)} + switch { + case m.Host != "": /* Domain */ + aType := socks5.AtypDomainName + length := uint8(len(m.Host)) + host := []byte(m.Host) + buf = [][]byte{{uint8(aType), length}, host, port} + case m.DstIP.To4() != nil: /* IPv4 */ + aType := socks5.AtypIPv4 + host := m.DstIP.To4() + buf = [][]byte{{uint8(aType)}, host, port} + default: /* IPv6 */ + aType := socks5.AtypIPv6 + host := m.DstIP.To16() + buf = [][]byte{{uint8(aType)}, host, port} + } + return bytes.Join(buf, nil) +} + +func (m *Metadata) Network() string { + return m.Net.String() +} + +func (m *Metadata) String() string { + return m.DestinationAddress() +} + +func (m *Metadata) Valid() bool { + return m.Host != "" || m.DstIP != nil +} diff --git a/internal/api/connections.go b/internal/api/connections.go new file mode 100644 index 0000000..a35e5c8 --- /dev/null +++ b/internal/api/connections.go @@ -0,0 +1,92 @@ +package api + +import ( + "bytes" + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + "github.com/gorilla/websocket" + + "github.com/xjasonlyu/tun2socks/internal/manager" +) + +func connectionRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getConnections) + r.Delete("/", closeAllConnections) + r.Delete("/{id}", closeConnection) + return r +} + +func getConnections(w http.ResponseWriter, r *http.Request) { + if !websocket.IsWebSocketUpgrade(r) { + snapshot := manager.DefaultManager.Snapshot() + render.JSON(w, r, snapshot) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + + intervalStr := r.URL.Query().Get("interval") + interval := 1000 + if intervalStr != "" { + t, err := strconv.Atoi(intervalStr) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + interval = t + } + + buf := &bytes.Buffer{} + sendSnapshot := func() error { + buf.Reset() + snapshot := manager.DefaultManager.Snapshot() + if err := json.NewEncoder(buf).Encode(snapshot); err != nil { + return err + } + + return conn.WriteMessage(websocket.TextMessage, buf.Bytes()) + } + + if err := sendSnapshot(); err != nil { + return + } + + tick := time.NewTicker(time.Millisecond * time.Duration(interval)) + defer tick.Stop() + for range tick.C { + if err := sendSnapshot(); err != nil { + break + } + } +} + +func closeConnection(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + snapshot := manager.DefaultManager.Snapshot() + for _, c := range snapshot.Connections { + if id == c.ID() { + _ = c.Close() + break + } + } + render.NoContent(w, r) +} + +func closeAllConnections(w http.ResponseWriter, r *http.Request) { + snapshot := manager.DefaultManager.Snapshot() + for _, c := range snapshot.Connections { + _ = c.Close() + } + render.NoContent(w, r) +} diff --git a/internal/api/errors.go b/internal/api/errors.go new file mode 100644 index 0000000..7a24f3a --- /dev/null +++ b/internal/api/errors.go @@ -0,0 +1,24 @@ +// Clash: https://github.com/Dreamacro/clash/blob/master/hub/route/errors.go + +package api + +var ( + ErrUnauthorized = newError("Unauthorized") + ErrBadRequest = newError("Body invalid") + ErrForbidden = newError("Forbidden") + ErrNotFound = newError("Resource not found") + ErrRequestTimeout = newError("Timeout") +) + +// HTTPError is custom HTTP error for API +type HTTPError struct { + Message string `json:"message"` +} + +func (e *HTTPError) Error() string { + return e.Message +} + +func newError(msg string) *HTTPError { + return &HTTPError{Message: msg} +} diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..d9c5cce --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,235 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/go-chi/render" + "github.com/gorilla/websocket" + "github.com/urfave/cli/v2" + + "github.com/xjasonlyu/tun2socks/internal/manager" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +const defaultScheme = "api" + +var ( + serverApp *cli.App + serverSecret = "" + serverAddr = "" + + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } +) + +func Start(apiURL string, app *cli.App) error { + if !strings.Contains(apiURL, "://") { + apiURL = defaultScheme + "://" + apiURL + } + + u, err := url.Parse(apiURL) + if err != nil { + return err + } + + if strings.ToLower(u.Scheme) != defaultScheme { + return errors.New("unsupported scheme") + } + serverApp = app + serverAddr = u.Host + serverSecret = u.Query().Get("secret") + + r := chi.NewRouter() + + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + MaxAge: 300, + }) + + r.Use(c.Handler) + + r.Group(func(r chi.Router) { + r.Use(authentication) + + r.Get("/", hello) + r.Get("/logs", getLogs) + r.Get("/traffic", traffic) + r.Get("/version", version) + r.Mount("/connections", connectionRouter()) + }) + + tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr) + if err != nil { + return err + } + + listener, err := net.ListenTCP("tcp", tcpAddr) + if err != nil { + return err + } + + go func() { + _ = http.Serve(listener, r) + }() + + return nil +} + +func authentication(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + if serverSecret == "" { + next.ServeHTTP(w, r) + return + } + + // Browser websocket not support custom header + if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" { + token := r.URL.Query().Get("token") + if token != serverSecret { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + return + } + + header := r.Header.Get("Authorization") + text := strings.SplitN(header, " ", 2) + + hasInvalidHeader := text[0] != "Bearer" + hasInvalidSecret := len(text) != 2 || text[1] != serverSecret + if hasInvalidHeader || hasInvalidSecret { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, ErrUnauthorized) + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +func getLogs(w http.ResponseWriter, r *http.Request) { + lvl := r.URL.Query().Get("level") + if lvl == "" { + lvl = "info" /* default */ + } + + level, err := log.ParseLevel(lvl) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + + var wsConn *websocket.Conn + if websocket.IsWebSocketUpgrade(r) { + wsConn, err = upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + } + + if wsConn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + sub := log.Subscribe() + defer log.UnSubscribe(sub) + + buf := &bytes.Buffer{} + for elm := range sub { + buf.Reset() + + e := elm.(*log.Event) + if e.Level > level { + continue + } + + if err := json.NewEncoder(buf).Encode(e); err != nil { + break + } + + if wsConn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + } + + if err != nil { + break + } + } +} + +func hello(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{"hello": serverApp.Name}) +} + +type Traffic struct { + Up int64 `json:"up"` + Down int64 `json:"down"` +} + +func traffic(w http.ResponseWriter, r *http.Request) { + var wsConn *websocket.Conn + if websocket.IsWebSocketUpgrade(r) { + var err error + wsConn, err = upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + } + + if wsConn == nil { + w.Header().Set("Content-Type", "application/json") + render.Status(r, http.StatusOK) + } + + tick := time.NewTicker(time.Second) + defer tick.Stop() + t := manager.DefaultManager + buf := &bytes.Buffer{} + var err error + for range tick.C { + buf.Reset() + up, down := t.Now() + if err := json.NewEncoder(buf).Encode(Traffic{ + Up: up, + Down: down, + }); err != nil { + break + } + + if wsConn == nil { + _, err = w.Write(buf.Bytes()) + w.(http.Flusher).Flush() + } else { + err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()) + } + + if err != nil { + break + } + } +} + +func version(w http.ResponseWriter, r *http.Request) { + render.JSON(w, r, render.M{"version": serverApp.Version}) +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 0000000..75cf3fd --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "fmt" + "os" + "os/signal" + "runtime" + "syscall" + "time" + + "github.com/urfave/cli/v2" + + "github.com/xjasonlyu/clash/component/dialer" + "github.com/xjasonlyu/tun2socks/internal/api" + "github.com/xjasonlyu/tun2socks/internal/core" + "github.com/xjasonlyu/tun2socks/internal/dev" + "github.com/xjasonlyu/tun2socks/internal/dns" + "github.com/xjasonlyu/tun2socks/internal/proxy" + "github.com/xjasonlyu/tun2socks/internal/tunnel" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +func bindToInterface(name string) { + dialer.DialHook = dialer.DialerWithInterface(name) + dialer.ListenPacketHook = dialer.ListenPacketWithInterface(name) +} + +func printVersion(app *cli.App) { + fmt.Printf("%s %s\n%s/%s, %s, %s\n", + app.Name, + app.Version, + runtime.GOOS, + runtime.GOARCH, + runtime.Version(), + app.Compiled.Format(time.RFC3339), + ) +} + +func Main(c *cli.Context) error { + if c.Bool("version") { + printVersion(c.App) + return nil + } + + level, err := log.ParseLevel(c.String("loglevel")) + if err != nil { + return err + } + log.SetLevel(level) + + if c.IsSet("interface") { + name := c.String("interface") + bindToInterface(name) + log.Infof("[IFCE] bind to interface: %s", name) + } + + if c.IsSet("api") { /* initiate API */ + raw := c.String("api") + if err := api.Start(raw, c.App); err != nil { + return fmt.Errorf("start API server %s: %w", raw, err) + } + log.Infof("[API] listen and serve at: %s", raw) + } + + if c.IsSet("dns") { /* initiate DNS */ + raw := c.String("dns") + if err := dns.Start(raw, c.StringSlice("hosts")); err != nil { + return fmt.Errorf("start DNS server %s: %w", raw, err) + } + log.Infof("[DNS] listen and serve at: %s", raw) + } + + deviceURL := c.String("device") + device, err := dev.Open(deviceURL) + if err != nil { + return fmt.Errorf("open device %s: %w", deviceURL, err) + } + defer func() { + err := device.Close() + if err != nil { + log.Errorf("close device %s error: %v", deviceURL, err) + } + }() + + proxyURL := c.String("proxy") + if err := proxy.Register(proxyURL); err != nil { + return fmt.Errorf("register proxy %s: %w", proxyURL, err) + } + + if _, err := core.NewDefaultStack(device, tunnel.Add, tunnel.AddPacket); err != nil { + return fmt.Errorf("initiate stack: %w", err) + } + log.Infof("[STACK] %s --> %s", device.String(), proxy.String()) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh + + return nil +} diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go new file mode 100644 index 0000000..9fc3655 --- /dev/null +++ b/internal/cmd/flags.go @@ -0,0 +1,51 @@ +package cmd + +import "github.com/urfave/cli/v2" + +var ( + API = cli.StringFlag{ + Name: "api", + Usage: "URL of external API to listen", + } + + Device = cli.StringFlag{ + Name: "device", + Aliases: []string{"d"}, + Usage: "URL of device to open", + } + + DNS = cli.StringFlag{ + Name: "dns", + Usage: "URL of fake DNS to listen", + } + + Hosts = cli.StringSliceFlag{ + Name: "hosts", + Usage: "Extra hosts mapping", + } + + Interface = cli.StringFlag{ + Name: "interface", + Aliases: []string{"i"}, + Usage: "Bind interface to dial", + } + + LogLevel = cli.StringFlag{ + Name: "loglevel", + Aliases: []string{"l"}, + Usage: "Set logging level", + Value: "INFO", + } + + Proxy = cli.StringFlag{ + Name: "proxy", + Aliases: []string{"p"}, + Usage: "URL of proxy to dial", + } + + Version = cli.BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "Print current version", + } +) diff --git a/internal/core/icmp.go b/internal/core/icmp.go new file mode 100644 index 0000000..682d2ff --- /dev/null +++ b/internal/core/icmp.go @@ -0,0 +1,43 @@ +package core + +import ( + "fmt" + "net" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type icmpHandleFunc func() + +func WithICMPHandler(_ icmpHandleFunc) Option { + return func(s *stack.Stack) error { + // Add default route table for IPv4 and IPv6 + // This will handle all incoming ICMP packets. + s.SetRouteTable([]tcpip.Route{ + { + Destination: mustSubnet("0.0.0.0/0"), + NIC: defaultNICID, + }, + { + Destination: mustSubnet("::/0"), + NIC: defaultNICID, + }, + }) + return nil + } +} + +// mustSubnet returns tcpip.Subnet from CIDR string. +func mustSubnet(s string) tcpip.Subnet { + _, ipNet, err := net.ParseCIDR(s) + if err != nil { + panic(fmt.Errorf("unable to ParseCIDR(%s): %w", s, err)) + } + + subnet, err := tcpip.NewSubnet(tcpip.Address(ipNet.IP), tcpip.AddressMask(ipNet.Mask)) + if err != nil { + panic(fmt.Errorf("unable to NewSubnet(%s): %w", ipNet, err)) + } + return subnet +} diff --git a/internal/core/nic.go b/internal/core/nic.go new file mode 100644 index 0000000..4ce62f2 --- /dev/null +++ b/internal/core/nic.go @@ -0,0 +1,52 @@ +package core + +import ( + "fmt" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +const ( + // defaultNICID is the ID of default NIC used by DefaultStack. + defaultNICID tcpip.NICID = 1 + + // nicPromiscuousModeEnabled is the value used by stack to enable + // or disable NIC's promiscuous mode. + nicPromiscuousModeEnabled = true + + // nicSpoofingEnabled is the value used by stack to enable or disable + // NIC's spoofing. + nicSpoofingEnabled = true +) + +// WithCreatingNIC creates NIC for stack. +func WithCreatingNIC(nicID tcpip.NICID, ep stack.LinkEndpoint) Option { + return func(s *stack.Stack) error { + if err := s.CreateNIC(nicID, ep); err != nil { + return fmt.Errorf("create NIC: %s", err) + } + return nil + } +} + +// WithPromiscuousMode sets promiscuous mode in the given NIC. +func WithPromiscuousMode(nicID tcpip.NICID, v bool) Option { + return func(s *stack.Stack) error { + if err := s.SetPromiscuousMode(nicID, v); err != nil { + return fmt.Errorf("set promiscuous mode: %s", err) + } + return nil + } +} + +// WithSpoofing sets address spoofing in the given NIC, allowing +// endpoints to bind to any address in the NIC. +func WithSpoofing(nicID tcpip.NICID, v bool) Option { + return func(s *stack.Stack) error { + if err := s.SetSpoofing(nicID, v); err != nil { + return fmt.Errorf("set spoofing: %s", err) + } + return nil + } +} diff --git a/internal/core/opts.go b/internal/core/opts.go new file mode 100644 index 0000000..2e80cea --- /dev/null +++ b/internal/core/opts.go @@ -0,0 +1,161 @@ +package core + +import ( + "fmt" + + "golang.org/x/time/rate" + "gvisor.dev/gvisor/pkg/tcpip" + "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/tcp" +) + +const ( + // maxBufferSize is the maximum permitted size of a send/receive buffer. + maxBufferSize = 4 << 20 // 4 MiB + + // minBufferSize is the smallest size of a receive or send buffer. + minBufferSize = 4 << 10 // 4 KiB + + // defaultBufferSize is the default size of the send/recv buffer for + // a transport endpoint. + defaultBufferSize = 212 << 10 // 212 KiB + + // defaultTimeToLive specifies the default TTL used by stack. + defaultTimeToLive uint8 = 64 + + // icmpBurst is the default number of ICMP messages that can be sent in + // a single burst. + icmpBurst = 50 + + // icmpLimit is the default maximum number of ICMP messages permitted + // by this rate limiter. + icmpLimit rate.Limit = 1000 + + // ipForwardingEnabled is the value used by stack to enable packet + // forwarding between NICs. + ipForwardingEnabled = true + + // tcpCongestionControl is the congestion control algorithm used by + // stack. ccReno is the default option in gVisor stack. + tcpCongestionControlAlgorithm = "reno" // "reno" or "cubic" + + // tcpDelayEnabled is the value used by stack to enable or disable + // tcp delay option. Disable Nagle's algorithm here by default. + tcpDelayEnabled = false + + // tcpModerateReceiveBufferEnabled is the value used by stack to + // enable or disable tcp receive buffer auto-tuning option. + tcpModerateReceiveBufferEnabled = true + + // tcpSACKEnabled is the value used by stack to enable or disable + // tcp selective ACK. + tcpSACKEnabled = true +) + +type Option func(*stack.Stack) error + +// WithDefaultTTL sets the default TTL used by stack. +func WithDefaultTTL(ttl uint8) Option { + return func(s *stack.Stack) error { + opt := tcpip.DefaultTTLOption(ttl) + if err := s.SetNetworkProtocolOption(ipv4.ProtocolNumber, &opt); err != nil { + return fmt.Errorf("set ipv4 default TTL: %s", err) + } + if err := s.SetNetworkProtocolOption(ipv6.ProtocolNumber, &opt); err != nil { + return fmt.Errorf("set ipv6 default TTL: %s", err) + } + return nil + } +} + +// WithForwarding sets packet forwarding between NICs for IPv4 & IPv6. +func WithForwarding(v bool) Option { + return func(s *stack.Stack) error { + if err := s.SetForwarding(ipv4.ProtocolNumber, v); err != nil { + return fmt.Errorf("set ipv4 forwarding: %s", err) + } + if err := s.SetForwarding(ipv6.ProtocolNumber, v); err != nil { + return fmt.Errorf("set ipv6 forwarding: %s", err) + } + return nil + } +} + +// WithICMPBurst sets the number of ICMP messages that can be sent +// in a single burst. +func WithICMPBurst(burst int) Option { + return func(s *stack.Stack) error { + s.SetICMPBurst(burst) + return nil + } +} + +// WithICMPLimit sets the maximum number of ICMP messages permitted +// by rate limiter. +func WithICMPLimit(limit rate.Limit) Option { + return func(s *stack.Stack) error { + s.SetICMPLimit(limit) + return nil + } +} + +// WithTCPBufferSizeRange sets the receive and send buffer size range for TCP. +func WithTCPBufferSizeRange(a, b, c int) Option { + return func(s *stack.Stack) error { + rcvOpt := tcpip.TCPReceiveBufferSizeRangeOption{Min: a, Default: b, Max: c} + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &rcvOpt); err != nil { + return fmt.Errorf("set TCP receive buffer size range: %s", err) + } + sndOpt := tcpip.TCPSendBufferSizeRangeOption{Min: a, Default: b, Max: c} + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &sndOpt); err != nil { + return fmt.Errorf("set TCP send buffer size range: %s", err) + } + return nil + } +} + +// WithTCPCongestionControl sets the current congestion control algorithm. +func WithTCPCongestionControl(cc string) Option { + return func(s *stack.Stack) error { + opt := tcpip.CongestionControlOption(cc) + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &opt); err != nil { + return fmt.Errorf("set TCP congestion control algorithm: %s", err) + } + return nil + } +} + +// WithTCPDelay enables or disables Nagle's algorithm in TCP. +func WithTCPDelay(v bool) Option { + return func(s *stack.Stack) error { + opt := tcpip.TCPDelayEnabled(v) + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &opt); err != nil { + return fmt.Errorf("set TCP delay: %s", err) + } + return nil + } +} + +// WithTCPModerateReceiveBuffer sets receive buffer moderation for TCP. +func WithTCPModerateReceiveBuffer(v bool) Option { + return func(s *stack.Stack) error { + opt := tcpip.TCPModerateReceiveBufferOption(v) + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &opt); err != nil { + return fmt.Errorf("set TCP moderate receive buffer: %s", err) + } + return nil + } +} + +// WithTCPSACKEnabled sets the SACK option for TCP. +func WithTCPSACKEnabled(v bool) Option { + return func(s *stack.Stack) error { + opt := tcpip.TCPSACKEnabled(v) + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &opt); err != nil { + return fmt.Errorf("set TCP SACK: %s", err) + } + return nil + } +} diff --git a/internal/core/stack.go b/internal/core/stack.go new file mode 100644 index 0000000..f92098a --- /dev/null +++ b/internal/core/stack.go @@ -0,0 +1,82 @@ +package core + +import ( + "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" +) + +// NewStack returns *stack.Stack with provided options. +func NewStack(opts ...Option) (*stack.Stack, error) { + ipstack := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ + ipv4.NewProtocol, + ipv6.NewProtocol, + }, + TransportProtocols: []stack.TransportProtocolFactory{ + tcp.NewProtocol, + udp.NewProtocol, + icmp.NewProtocol4, + icmp.NewProtocol6, + }, + }) + + for _, opt := range opts { + if err := opt(ipstack); err != nil { + return nil, err + } + } + + return ipstack, nil +} + +// NewDefaultStack calls NewStack with default options. +func NewDefaultStack(linkEp stack.LinkEndpoint, th tcpHandleFunc, uh udpHandleFunc) (*stack.Stack, error) { + return NewStack( + WithDefaultTTL(defaultTimeToLive), + WithForwarding(ipForwardingEnabled), + + // Config default stack ICMP settings. + WithICMPBurst(icmpBurst), WithICMPLimit(icmpLimit), + + // We expect no packet loss, therefore we can bump buffers. + // Too large buffers thrash cache, so there is little point + // in too large buffers. + // + // Ref: https://github.com/majek/slirpnetstack/blob/master/stack.go + WithTCPBufferSizeRange(minBufferSize, defaultBufferSize, maxBufferSize), + + WithTCPCongestionControl(tcpCongestionControlAlgorithm), + WithTCPDelay(tcpDelayEnabled), + + // Receive Buffer Auto-Tuning Option, see: + // https://github.com/google/gvisor/issues/1666 + WithTCPModerateReceiveBuffer(tcpModerateReceiveBufferEnabled), + + // TCP selective ACK Option, see: + // https://tools.ietf.org/html/rfc2018 + WithTCPSACKEnabled(tcpSACKEnabled), + + // Important: We must initiate transport protocol handlers + // before creating NIC, otherwise NIC would dispatch packets + // to stack and cause race condition. + WithICMPHandler(nil), WithTCPHandler(th), WithUDPHandler(uh), + + // Create stack NIC and then bind link endpoint. + WithCreatingNIC(defaultNICID, linkEp), + + // In past we did s.AddAddressRange to assign 0.0.0.0/0 onto + // the interface. We need that to be able to terminate all the + // incoming connections - to any ip. AddressRange API has been + // removed and the suggested workaround is to use Promiscuous + // mode. https://github.com/google/gvisor/issues/3876 + // + // Ref: https://github.com/majek/slirpnetstack/blob/master/stack.go + WithPromiscuousMode(defaultNICID, nicPromiscuousModeEnabled), + + WithSpoofing(defaultNICID, nicSpoofingEnabled), + ) +} diff --git a/internal/core/tcp.go b/internal/core/tcp.go new file mode 100644 index 0000000..a6b048f --- /dev/null +++ b/internal/core/tcp.go @@ -0,0 +1,108 @@ +package core + +import ( + "fmt" + "net" + "time" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/waiter" + + "github.com/xjasonlyu/tun2socks/internal/adapter" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +const ( + // defaultWndSize if set to zero, the default + // receive window buffer size is used instead. + defaultWndSize = 0 + + // maxConnAttempts specifies the maximum number + // of in-flight tcp connection attempts. + maxConnAttempts = 2 << 10 + + // tcpKeepaliveIdle specifies the time a connection + // must remain idle before the first TCP keepalive + // packet is sent. Once this time is reached, + // tcpKeepaliveInterval option is used instead. + tcpKeepaliveIdle = 60 * time.Second + + // tcpKeepaliveInterval specifies the interval + // time between sending TCP keepalive packets. + tcpKeepaliveInterval = 30 * time.Second +) + +type tcpHandleFunc func(adapter.TCPConn) + +func WithTCPHandler(handle tcpHandleFunc) Option { + return func(s *stack.Stack) error { + tcpForwarder := tcp.NewForwarder(s, defaultWndSize, maxConnAttempts, func(r *tcp.ForwarderRequest) { + var wq waiter.Queue + id := r.ID() + ep, err := r.CreateEndpoint(&wq) + if err != nil { + log.Warnf("[STACK] %s create endpoint error: %v", formatID(&id), err) + // prevent potential half-open TCP connection leak. + r.Complete(true) + return + } + r.Complete(false) + + if err := setKeepalive(ep); err != nil { + log.Warnf("[STACK] %s %v", formatID(&id), err) + } + + conn := &tcpConn{ + Conn: gonet.NewTCPConn(&wq, ep), + metadata: &adapter.Metadata{ + Net: adapter.TCP, + SrcIP: net.IP(id.RemoteAddress), + SrcPort: id.RemotePort, + DstIP: net.IP(id.LocalAddress), + DstPort: id.LocalPort, + }, + } + + handle(conn) + }) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + return nil + } +} + +func formatID(id *stack.TransportEndpointID) string { + return fmt.Sprintf( + "%s:%d --> %s:%d", + id.RemoteAddress, + id.RemotePort, + id.LocalAddress, + id.LocalPort, + ) +} + +func setKeepalive(ep tcpip.Endpoint) error { + if err := ep.SetSockOptBool(tcpip.KeepaliveEnabledOption, true); err != nil { + return fmt.Errorf("set keepalive: %s", err) + } + idleOpt := tcpip.KeepaliveIdleOption(tcpKeepaliveIdle) + if err := ep.SetSockOpt(&idleOpt); err != nil { + return fmt.Errorf("set keepalive idle: %s", err) + } + intervalOpt := tcpip.KeepaliveIntervalOption(tcpKeepaliveInterval) + if err := ep.SetSockOpt(&intervalOpt); err != nil { + return fmt.Errorf("set keepalive interval: %s", err) + } + return nil +} + +type tcpConn struct { + net.Conn + metadata *adapter.Metadata +} + +func (c *tcpConn) Metadata() *adapter.Metadata { + return c.metadata +} diff --git a/internal/core/udp.go b/internal/core/udp.go new file mode 100644 index 0000000..10847b3 --- /dev/null +++ b/internal/core/udp.go @@ -0,0 +1,179 @@ +package core + +import ( + "fmt" + "net" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/buffer" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +const udpNoChecksum = true + +type udpHandleFunc func(adapter.UDPPacket) + +func WithUDPHandler(handle udpHandleFunc) Option { + return func(s *stack.Stack) error { + udpHandlePacket := func(r *stack.Route, id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + // Ref: gVisor pkg/tcpip/transport/udp/endpoint.go HandlePacket + hdr := header.UDP(pkt.TransportHeader().View()) + if int(hdr.Length()) > pkt.Data.Size()+header.UDPMinimumSize { + // Malformed packet. + s.Stats().UDP.MalformedPacketsReceived.Increment() + return true + } + + if !verifyChecksum(r, hdr, pkt) { + // Checksum error. + s.Stats().UDP.ChecksumErrors.Increment() + return true + } + + s.Stats().UDP.PacketsReceived.Increment() + + // make a clone here. + route := r.Clone() + packet := &udpPacket{ + id: id, + r: &route, + metadata: &adapter.Metadata{ + Net: adapter.UDP, + SrcIP: net.IP(id.RemoteAddress), + SrcPort: id.RemotePort, + DstIP: net.IP(id.LocalAddress), + DstPort: id.LocalPort, + }, + payload: pkt.Data.ToView(), + } + + handle(packet) + return true + } + s.SetTransportProtocolHandler(udp.ProtocolNumber, udpHandlePacket) + return nil + } +} + +type udpPacket struct { + id stack.TransportEndpointID + r *stack.Route + metadata *adapter.Metadata + payload []byte +} + +func (p *udpPacket) Data() []byte { + return p.payload +} + +func (p *udpPacket) Drop() { + p.r.Release() +} + +func (p *udpPacket) LocalAddr() net.Addr { + return &net.UDPAddr{IP: net.IP(p.id.LocalAddress), Port: int(p.id.LocalPort)} +} + +func (p *udpPacket) Metadata() *adapter.Metadata { + return p.metadata +} + +func (p *udpPacket) RemoteAddr() net.Addr { + return &net.UDPAddr{IP: net.IP(p.id.RemoteAddress), Port: int(p.id.RemotePort)} +} + +func (p *udpPacket) WriteBack(b []byte, addr net.Addr) (int, error) { + v := buffer.View(b) + if len(v) > header.UDPMaximumPacketSize { + // Payload can't possibly fit in a packet. + return 0, fmt.Errorf("%s", tcpip.ErrMessageTooLong) + } + + data := v.ToVectorisedView() + // if addr is not provided, write back use original dst Addr as src Addr. + if addr == nil { + return sendUDP(p.r, data, p.id.LocalPort, p.id.RemotePort, udpNoChecksum) + } + + udpAddr, ok := addr.(*net.UDPAddr) + if !ok { + return 0, fmt.Errorf("type %T is not a valid udp address", addr) + } + + r := p.r.Clone() + defer r.Release() + if ipv4 := udpAddr.IP.To4(); ipv4 != nil { + r.LocalAddress = tcpip.Address(ipv4) + } else { + r.LocalAddress = tcpip.Address(udpAddr.IP) + } + return sendUDP(&r, data, uint16(udpAddr.Port), p.id.RemotePort, udpNoChecksum) +} + +// sendUDP sends a UDP segment via the provided network endpoint and under the +// provided identity. +func sendUDP(r *stack.Route, data buffer.VectorisedView, localPort, remotePort uint16, noChecksum bool) (int, error) { + // Allocate a buffer for the UDP header. + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + ReserveHeaderBytes: header.UDPMinimumSize + int(r.MaxHeaderLength()), + Data: data, + }) + + // Initialize the UDP header. + udpHdr := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize)) + pkt.TransportProtocolNumber = udp.ProtocolNumber + + length := uint16(pkt.Size()) + udpHdr.Encode(&header.UDPFields{ + SrcPort: localPort, + DstPort: remotePort, + Length: length, + }) + + // Set the checksum field unless TX checksum offload is enabled. + // On IPv4, UDP checksum is optional, and a zero value indicates the + // transmitter skipped the checksum generation (RFC768). + // On IPv6, UDP checksum is not optional (RFC2460 Section 8.1). + if r.Capabilities()&stack.CapabilityTXChecksumOffload == 0 && + (!noChecksum || r.NetProto == header.IPv6ProtocolNumber) { + xsum := r.PseudoHeaderChecksum(udp.ProtocolNumber, length) + for _, v := range data.Views() { + xsum = header.Checksum(v, xsum) + } + udpHdr.SetChecksum(^udpHdr.CalculateChecksum(xsum)) + } + + ttl := r.DefaultTTL() + if err := r.WritePacket(nil /* gso */, stack.NetworkHeaderParams{ + Protocol: udp.ProtocolNumber, + TTL: ttl, + TOS: 0, /* default */ + }, pkt); err != nil { + r.Stats().UDP.PacketSendErrors.Increment() + return 0, fmt.Errorf("%s", err) + } + + // Track count of packets sent. + r.Stats().UDP.PacketsSent.Increment() + return data.Size(), nil +} + +// verifyChecksum verifies the checksum unless RX checksum offload is enabled. +// On IPv4, UDP checksum is optional, and a zero value means the transmitter +// omitted the checksum generation (RFC768). +// On IPv6, UDP checksum is not optional (RFC2460 Section 8.1). +func verifyChecksum(r *stack.Route, hdr header.UDP, pkt *stack.PacketBuffer) bool { + if r.Capabilities()&stack.CapabilityRXChecksumOffload == 0 && + (hdr.Checksum() != 0 || r.NetProto == header.IPv6ProtocolNumber) { + xsum := r.PseudoHeaderChecksum(udp.ProtocolNumber, hdr.Length()) + for _, v := range pkt.Data.Views() { + xsum = header.Checksum(v, xsum) + } + return hdr.CalculateChecksum(xsum) == 0xffff + } + return true +} diff --git a/internal/dev/dev.go b/internal/dev/dev.go new file mode 100644 index 0000000..ea4f8f8 --- /dev/null +++ b/internal/dev/dev.go @@ -0,0 +1,74 @@ +package dev + +import ( + "errors" + "io" + "net/url" + "strings" + + "gvisor.dev/gvisor/pkg/tcpip/stack" + + "github.com/xjasonlyu/tun2socks/internal/dev/tun" +) + +const defaultScheme = "tun" + +type Device struct { + url *url.URL + io.Closer + stack.LinkEndpoint +} + +func Open(deviceURL string) (device *Device, err error) { + if !strings.Contains(deviceURL, "://") { + deviceURL = defaultScheme + "://" + deviceURL + } + + var u *url.URL + if u, err = url.Parse(deviceURL); err != nil { + return + } + + var ( + ep stack.LinkEndpoint + c io.Closer + ) + switch strings.ToLower(u.Scheme) { + case "tun": + name := u.Host + ep, c, err = tun.Open(name) + default: + err = errors.New("unsupported device type") + } + + if err != nil { + return + } + + device = &Device{ + url: u, + Closer: c, + LinkEndpoint: ep, + } + return +} + +// Close closes device. +func (d *Device) Close() error { + return d.Closer.Close() +} + +// Name returns name of device. +func (d *Device) Name() string { + return d.url.Host +} + +// Type returns type of device. +func (d *Device) Type() string { + return strings.ToLower(d.url.Scheme) +} + +// String returns full URL string. +func (d *Device) String() string { + return d.url.String() +} diff --git a/internal/dev/tun/tun.go b/internal/dev/tun/tun.go new file mode 100644 index 0000000..a412197 --- /dev/null +++ b/internal/dev/tun/tun.go @@ -0,0 +1,15 @@ +// +build !darwin,!linux + +package tun + +import ( + "fmt" + "io" + "runtime" + + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +func Open(_ string) (stack.LinkEndpoint, io.Closer, error) { + return nil, nil, fmt.Errorf("operation was not supported on %s", runtime.GOOS) +} diff --git a/internal/dev/tun/tun_darwin.go b/internal/dev/tun/tun_darwin.go new file mode 100644 index 0000000..a902ed3 --- /dev/null +++ b/internal/dev/tun/tun_darwin.go @@ -0,0 +1,70 @@ +package tun + +import ( + "fmt" + "io" + "syscall" + "unsafe" + + "github.com/songgao/water" + "gvisor.dev/gvisor/pkg/tcpip/stack" + + "github.com/xjasonlyu/tun2socks/pkg/link/rwc" +) + +func Open(name string) (ep stack.LinkEndpoint, c io.Closer, err error) { + config := water.Config{ + DeviceType: water.TUN, + } + config.Name = name + + var ifce *water.Interface + ifce, err = water.New(config) + if err != nil { + return + } + + var mtu uint32 + mtu, err = getMTU(name) + if err != nil { + return + } + + ep, err = rwc.New(ifce, mtu) + c = ifce + + return +} + +func getMTU(name string) (uint32, error) { + // open datagram socket + fd, err := syscall.Socket( + syscall.AF_INET, + syscall.SOCK_DGRAM, + 0, + ) + if err != nil { + return 0, err + } + + defer syscall.Close(fd) + + // do ioctl call + var ifr struct { + name [16]byte + mtu uint32 + } + copy(ifr.name[:], name) + + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + uintptr(fd), + uintptr(syscall.SIOCGIFMTU), + uintptr(unsafe.Pointer(&ifr)), + ) + if errno != 0 { + return 0, fmt.Errorf("get MTU on %s: %s", name, errno.Error()) + } + + return ifr.mtu, nil +} diff --git a/internal/dev/tun/tun_linux.go b/internal/dev/tun/tun_linux.go new file mode 100644 index 0000000..126593f --- /dev/null +++ b/internal/dev/tun/tun_linux.go @@ -0,0 +1,40 @@ +package tun + +import ( + "io" + "syscall" + + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/link/rawfile" + "gvisor.dev/gvisor/pkg/tcpip/link/tun" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type closeFunc func() error + +func (f closeFunc) Close() error { + return f() +} + +func Open(name string) (ep stack.LinkEndpoint, c io.Closer, err error) { + var fd int + fd, err = tun.Open(name) + + var mtu uint32 + mtu, err = rawfile.GetMTU(name) + if err != nil { + return + } + + ep, err = fdbased.New(&fdbased.Options{ + FDs: []int{fd}, + MTU: mtu, + EthernetHeader: false, + }) + + c = closeFunc(func() error { + return syscall.Close(fd) + }) + + return +} diff --git a/internal/dns/resolver.go b/internal/dns/resolver.go new file mode 100644 index 0000000..cf8a377 --- /dev/null +++ b/internal/dns/resolver.go @@ -0,0 +1,12 @@ +package dns + +import ( + "net" + + "github.com/xjasonlyu/clash/component/dialer" +) + +func init() { /* use bound dialer to resolve DNS */ + net.DefaultResolver.PreferGo = true + net.DefaultResolver.Dial = dialer.DialContext +} diff --git a/internal/dns/server.go b/internal/dns/server.go new file mode 100644 index 0000000..bc21f56 --- /dev/null +++ b/internal/dns/server.go @@ -0,0 +1,107 @@ +package dns + +import ( + "errors" + "net/url" + "strings" + + "github.com/xjasonlyu/clash/component/resolver" + "github.com/xjasonlyu/clash/dns" +) + +const ( + defaultScheme = "dns" + defaultCacheSize = 1000 + defaultFakeIPRange = "198.18.0.0/15" +) + +var _defaultNameServer = []string{"223.5.5.5", "8.8.8.8"} + +func Start(dnsURL string, rawHosts []string) error { + if !strings.Contains(dnsURL, "://") { + dnsURL = defaultScheme + "://" + dnsURL + } + + u, err := url.Parse(dnsURL) + if err != nil { + return err + } + + if strings.ToLower(u.Scheme) != defaultScheme { + return errors.New("unsupported scheme") + } + + var serverAddr = u.Host + + fakeIPRange := u.Query().Get("fake-ip-range") + if fakeIPRange == "" { + fakeIPRange = defaultFakeIPRange + } + + var fakeIPFilter []string + if raw := strings.TrimSpace(u.Query().Get("fake-ip-filter")); raw != "" { + fakeIPFilter = append(fakeIPFilter, strings.Split(raw, ",")...) + } + + pool, err := parseFakeIP(fakeIPRange, fakeIPFilter) + if err != nil { + return err + } + + var mainNS, defaultNS []dns.NameServer + { + var ns []string + if raw := strings.TrimSpace(u.Query().Get("default-nameserver")); raw != "" { + ns = append(ns, strings.Split(raw, ",")...) + } + + if len(ns) == 0 { + ns = append(ns, _defaultNameServer...) + } + + if defaultNS, err = parseNameServer(ns); err != nil { + return err + } + } + { + var ns []string + if raw := strings.TrimSpace(u.Query().Get("nameserver")); raw != "" { + ns = append(ns, strings.Split(raw, ",")...) + } + + if len(ns) == 0 { + ns = append(ns, _defaultNameServer...) + } + + if mainNS, err = parseNameServer(ns); err != nil { + return err + } + } + + hosts, err := parseHosts(parseHostsSlice(rawHosts)) + if err != nil { + return err + } + + cfg := dns.Config{ + IPv6: true, + Pool: pool, + Hosts: hosts, + Main: mainNS, + Default: defaultNS, + EnhancedMode: dns.FAKEIP, + } + + r := dns.NewResolver(cfg) + m := dns.NewEnhancer(cfg) + + // reuse cache of old host mapper + if old := resolver.DefaultHostMapper; old != nil { + m.PatchFrom(old.(*dns.ResolverEnhancer)) + } + + resolver.DefaultResolver = r + resolver.DefaultHostMapper = m + + return dns.ReCreateServer(serverAddr, r, m) +} diff --git a/internal/dns/utils.go b/internal/dns/utils.go new file mode 100644 index 0000000..284e703 --- /dev/null +++ b/internal/dns/utils.go @@ -0,0 +1,141 @@ +package dns + +import ( + "fmt" + "net" + "net/url" + "strings" + + "github.com/xjasonlyu/clash/component/fakeip" + "github.com/xjasonlyu/clash/component/trie" + "github.com/xjasonlyu/clash/dns" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +func parseFakeIP(fakeIPRange string, fakeIPFilter []string) (*fakeip.Pool, error) { + _, ipnet, err := net.ParseCIDR(fakeIPRange) + if err != nil { + return nil, err + } + + var host *trie.DomainTrie + // fake ip skip host filter + if len(fakeIPFilter) != 0 { + host = trie.New() + for _, domain := range fakeIPFilter { + _ = host.Insert(domain, true) + } + } + + return fakeip.New(ipnet, defaultCacheSize, host) +} + +func parseHosts(hosts map[string]string) (*trie.DomainTrie, error) { + tree := trie.New() + + // add default hosts + if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil { + log.Errorf("[DNS] insert localhost to host error: %v", err) + } + + if len(hosts) != 0 { + for domain, ipStr := range hosts { + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("%s is not a valid IP", ipStr) + } + _ = tree.Insert(domain, ip) + } + } + + return tree, nil +} + +func parseHostsSlice(s []string) map[string]string { + m := make(map[string]string) + + for _, i := range s { + if strings.Contains(i, "=") { + v := strings.SplitN(i, "=", 2) + m[v[0]] = v[1] + continue + } + if strings.Contains(i, ":") { + v := strings.SplitN(i, ":", 2) + m[v[0]] = v[1] + continue + } + log.Debugf("invalid hosts item: %s", i) + } + + return m +} + +func hostWithDefaultPort(host string, defPort string) (string, error) { + if !strings.Contains(host, ":") { + host += ":" + } + + hostname, port, err := net.SplitHostPort(host) + if err != nil { + return "", err + } + + if port == "" { + port = defPort + } + + return net.JoinHostPort(hostname, port), nil +} + +func parseNameServer(servers []string) ([]dns.NameServer, error) { + var nameservers []dns.NameServer + + for idx, server := range servers { + server := strings.TrimSpace(server) + if server == "" { + return nil, fmt.Errorf("empty server field") + } + + // parse without scheme .e.g 8.8.8.8:53 + if !strings.Contains(server, "://") { + server = "udp://" + server + } + u, err := url.Parse(server) + if err != nil { + return nil, fmt.Errorf("DNS NameServer[%d] format: %s", idx, err.Error()) + } + + var addr, dnsNetType string + switch u.Scheme { + case "udp": + addr, err = hostWithDefaultPort(u.Host, "53") + dnsNetType = "" // UDP + case "tcp": + addr, err = hostWithDefaultPort(u.Host, "53") + dnsNetType = "tcp" // TCP + case "tls": + addr, err = hostWithDefaultPort(u.Host, "853") + dnsNetType = "tcp-tls" // DNS over TLS + case "https": + clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path} + addr = clearURL.String() + dnsNetType = "https" // DNS over HTTPS + default: + return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) + } + + if err != nil { + return nil, fmt.Errorf("DNS NameServer[%d] format: %s", idx, err.Error()) + } + + nameservers = append( + nameservers, + dns.NameServer{ + Net: dnsNetType, + Addr: addr, + }, + ) + } + return nameservers, nil +} diff --git a/internal/manager/manager.go b/internal/manager/manager.go new file mode 100644 index 0000000..7081801 --- /dev/null +++ b/internal/manager/manager.go @@ -0,0 +1,95 @@ +package manager + +import ( + "sync" + "time" + + "go.uber.org/atomic" +) + +var DefaultManager *Manager + +func init() { + DefaultManager = &Manager{ + uploadTemp: atomic.NewInt64(0), + downloadTemp: atomic.NewInt64(0), + uploadBlip: atomic.NewInt64(0), + downloadBlip: atomic.NewInt64(0), + uploadTotal: atomic.NewInt64(0), + downloadTotal: atomic.NewInt64(0), + } + + go DefaultManager.handle() +} + +type Manager struct { + connections sync.Map + uploadTemp *atomic.Int64 + downloadTemp *atomic.Int64 + uploadBlip *atomic.Int64 + downloadBlip *atomic.Int64 + uploadTotal *atomic.Int64 + downloadTotal *atomic.Int64 +} + +func (m *Manager) Join(c tracker) { + m.connections.Store(c.ID(), c) +} + +func (m *Manager) Leave(c tracker) { + m.connections.Delete(c.ID()) +} + +func (m *Manager) PushUploaded(size int64) { + m.uploadTemp.Add(size) + m.uploadTotal.Add(size) +} + +func (m *Manager) PushDownloaded(size int64) { + m.downloadTemp.Add(size) + m.downloadTotal.Add(size) +} + +func (m *Manager) Now() (up int64, down int64) { + return m.uploadBlip.Load(), m.downloadBlip.Load() +} + +func (m *Manager) Snapshot() *Snapshot { + var connections []tracker + m.connections.Range(func(key, value interface{}) bool { + connections = append(connections, value.(tracker)) + return true + }) + + return &Snapshot{ + UploadTotal: m.uploadTotal.Load(), + DownloadTotal: m.downloadTotal.Load(), + Connections: connections, + } +} + +func (m *Manager) ResetStatistic() { + m.uploadTemp.Store(0) + m.uploadBlip.Store(0) + m.uploadTotal.Store(0) + m.downloadTemp.Store(0) + m.downloadBlip.Store(0) + m.downloadTotal.Store(0) +} + +func (m *Manager) handle() { + ticker := time.NewTicker(time.Second) + + for range ticker.C { + m.uploadBlip.Store(m.uploadTemp.Load()) + m.uploadTemp.Store(0) + m.downloadBlip.Store(m.downloadTemp.Load()) + m.downloadTemp.Store(0) + } +} + +type Snapshot struct { + DownloadTotal int64 `json:"downloadTotal"` + UploadTotal int64 `json:"uploadTotal"` + Connections []tracker `json:"connections"` +} diff --git a/internal/manager/tracker.go b/internal/manager/tracker.go new file mode 100644 index 0000000..06930d1 --- /dev/null +++ b/internal/manager/tracker.go @@ -0,0 +1,126 @@ +package manager + +import ( + "net" + "time" + + "github.com/gofrs/uuid" + "go.uber.org/atomic" + + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +type tracker interface { + ID() string + Close() error +} + +type trackerInfo struct { + Start time.Time `json:"start"` + UUID uuid.UUID `json:"id"` + Metadata *adapter.Metadata `json:"metadata"` + UploadTotal *atomic.Int64 `json:"upload"` + DownloadTotal *atomic.Int64 `json:"download"` +} + +type tcpTracker struct { + net.Conn `json:"-"` + + *trackerInfo + manager *Manager +} + +func NewTCPTracker(conn net.Conn, metadata *adapter.Metadata) *tcpTracker { + id, _ := uuid.NewV4() + + t := &tcpTracker{ + Conn: conn, + manager: DefaultManager, + trackerInfo: &trackerInfo{ + UUID: id, + Start: time.Now(), + Metadata: metadata, + UploadTotal: atomic.NewInt64(0), + DownloadTotal: atomic.NewInt64(0), + }, + } + + DefaultManager.Join(t) + return t +} + +func (tt *tcpTracker) ID() string { + return tt.UUID.String() +} + +func (tt *tcpTracker) Read(b []byte) (int, error) { + n, err := tt.Conn.Read(b) + download := int64(n) + tt.manager.PushDownloaded(download) + tt.DownloadTotal.Add(download) + return n, err +} + +func (tt *tcpTracker) Write(b []byte) (int, error) { + n, err := tt.Conn.Write(b) + upload := int64(n) + tt.manager.PushUploaded(upload) + tt.UploadTotal.Add(upload) + return n, err +} + +func (tt *tcpTracker) Close() error { + tt.manager.Leave(tt) + return tt.Conn.Close() +} + +type udpTracker struct { + net.PacketConn `json:"-"` + + *trackerInfo + manager *Manager +} + +func NewUDPTracker(conn net.PacketConn, metadata *adapter.Metadata) *udpTracker { + id, _ := uuid.NewV4() + + ut := &udpTracker{ + PacketConn: conn, + manager: DefaultManager, + trackerInfo: &trackerInfo{ + UUID: id, + Start: time.Now(), + Metadata: metadata, + UploadTotal: atomic.NewInt64(0), + DownloadTotal: atomic.NewInt64(0), + }, + } + + DefaultManager.Join(ut) + return ut +} + +func (ut *udpTracker) ID() string { + return ut.UUID.String() +} + +func (ut *udpTracker) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := ut.PacketConn.ReadFrom(b) + download := int64(n) + ut.manager.PushDownloaded(download) + ut.DownloadTotal.Add(download) + return n, addr, err +} + +func (ut *udpTracker) WriteTo(b []byte, addr net.Addr) (int, error) { + n, err := ut.PacketConn.WriteTo(b, addr) + upload := int64(n) + ut.manager.PushUploaded(upload) + ut.UploadTotal.Add(upload) + return n, err +} + +func (ut *udpTracker) Close() error { + ut.manager.Leave(ut) + return ut.PacketConn.Close() +} diff --git a/internal/proxy/base.go b/internal/proxy/base.go new file mode 100755 index 0000000..f2af7a1 --- /dev/null +++ b/internal/proxy/base.go @@ -0,0 +1,50 @@ +package proxy + +import ( + "context" + "errors" + "net" + "net/url" + "strings" + + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +type Base struct { + url *url.URL +} + +func NewBase(url *url.URL) (*Base, error) { + return &Base{ + url: url, + }, nil +} + +func (b *Base) Type() string { + if b.url == nil { + return "" + } + return strings.ToLower(b.url.Scheme) +} + +func (b *Base) Addr() string { + if b.url == nil { + return "" + } + return b.url.Host +} + +func (b *Base) String() string { + if b.url == nil { + return "" + } + return b.url.String() +} + +func (b *Base) DialContext(context.Context, *adapter.Metadata) (net.Conn, error) { + return nil, errors.New("no support") +} + +func (b *Base) DialUDP(*adapter.Metadata) (net.PacketConn, error) { + return nil, errors.New("no support") +} diff --git a/internal/proxy/direct.go b/internal/proxy/direct.go new file mode 100755 index 0000000..f8730f8 --- /dev/null +++ b/internal/proxy/direct.go @@ -0,0 +1,57 @@ +package proxy + +import ( + "context" + "net" + "net/url" + + "github.com/xjasonlyu/clash/component/dialer" + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +type Direct struct { + *Base +} + +func NewDirect(url *url.URL) (*Direct, error) { + return &Direct{ + Base: &Base{ + url: url, + }, + }, nil +} + +func (d *Direct) DialContext(ctx context.Context, metadata *adapter.Metadata) (net.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", metadata.DestinationAddress()) + if err != nil { + return nil, err + } + tcpKeepAlive(c) + return c, nil +} + +func (d *Direct) DialUDP(_ *adapter.Metadata) (net.PacketConn, error) { + pc, err := dialer.ListenPacket("udp", "") + if err != nil { + return nil, err + } + return &directPacketConn{PacketConn: pc}, nil +} + +type directPacketConn struct { + net.PacketConn +} + +func (pc *directPacketConn) WriteTo(b []byte, addr net.Addr) (_ int, err error) { + var udpAddr *net.UDPAddr + if m, ok := addr.(*adapter.Metadata); ok && m.Host == "" { + udpAddr = m.UDPAddr() + } else { + udpAddr, err = resolveUDPAddr("udp", addr.String()) + } + + if err != nil { + return 0, err + } + return pc.PacketConn.WriteTo(b, udpAddr) +} diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go new file mode 100755 index 0000000..5748deb --- /dev/null +++ b/internal/proxy/proxy.go @@ -0,0 +1,84 @@ +package proxy + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +type Dialer interface { + Addr() string + Type() string + String() string + + DialContext(context.Context, *adapter.Metadata) (net.Conn, error) + DialUDP(*adapter.Metadata) (net.PacketConn, error) +} + +var _defaultDialer Dialer = &Base{} + +// New returns proxy dialer. +func New(proxyURL string) (Dialer, error) { + u, err := url.Parse(proxyURL) + if err != nil { + return nil, err + } + + proto := strings.ToLower(u.Scheme) + user := u.User.Username() + pass, _ := u.User.Password() + + switch proto { + case "direct": + return NewDirect(u) + case "socks5": + return NewSocks5(u, user, pass) + case "ss", "shadowsocks": + method, password := user, pass + return NewShadowSocks(u, method, password) + } + + return nil, fmt.Errorf("unsupported protocol: %s", proto) +} + +// Register updates the _defaultDialer. +func Register(proxyURL string) error { + dialer, err := New(proxyURL) + if err != nil { + return err + } + + _defaultDialer = dialer + return nil +} + +// Dial uses _defaultDialer to dial TCP. +func Dial(metadata *adapter.Metadata) (net.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout) + defer cancel() + return _defaultDialer.DialContext(ctx, metadata) +} + +// DialUDP uses _defaultDialer to dial UDP. +func DialUDP(metadata *adapter.Metadata) (net.PacketConn, error) { + return _defaultDialer.DialUDP(metadata) +} + +// Addr returns _defaultDialer addr. +func Addr() string { + return _defaultDialer.Addr() +} + +// Type returns _defaultDialer type. +func Type() string { + return _defaultDialer.Type() +} + +// String returns _defaultDialer URL. +func String() string { + return _defaultDialer.String() +} diff --git a/internal/proxy/shadowsocks.go b/internal/proxy/shadowsocks.go new file mode 100755 index 0000000..5ad0191 --- /dev/null +++ b/internal/proxy/shadowsocks.go @@ -0,0 +1,108 @@ +package proxy + +import ( + "context" + "errors" + "fmt" + "net" + "net/url" + + "github.com/Dreamacro/go-shadowsocks2/core" + + "github.com/xjasonlyu/clash/component/dialer" + "github.com/xjasonlyu/clash/component/socks5" + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +type ShadowSocks struct { + *Base + + cipher core.Cipher +} + +func NewShadowSocks(url *url.URL, method, password string) (*ShadowSocks, error) { + cipher, err := core.PickCipher(method, nil, password) + if err != nil { + return nil, fmt.Errorf("ss initialize: %w", err) + } + + return &ShadowSocks{ + Base: &Base{ + url: url, + }, + cipher: cipher, + }, nil +} + +func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *adapter.Metadata) (c net.Conn, err error) { + c, err = dialer.DialContext(ctx, "tcp", ss.Addr()) + if err != nil { + return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err) + } + tcpKeepAlive(c) + + defer func() { + if err != nil { + c.Close() + } + }() + + c = ss.cipher.StreamConn(c) + _, err = c.Write(metadata.SerializesSocksAddr()) + return +} + +func (ss *ShadowSocks) DialUDP(_ *adapter.Metadata) (net.PacketConn, error) { + pc, err := dialer.ListenPacket("udp", "") + if err != nil { + return nil, err + } + + udpAddr, err := resolveUDPAddr("udp", ss.Addr()) + if err != nil { + return nil, fmt.Errorf("resolve UDPAddr %s failed", ss.Addr()) + } + + pc = ss.cipher.PacketConn(pc) + return &ssPacketConn{PacketConn: pc, rAddr: udpAddr}, nil +} + +type ssPacketConn struct { + net.PacketConn + + rAddr net.Addr +} + +func (pc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + var packet []byte + if m, ok := addr.(*adapter.Metadata); ok { + packet, err = socks5.EncodeUDPPacket(m.SerializesSocksAddr(), b) + } else { + packet, err = socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) + } + + if err != nil { + return + } + return pc.PacketConn.WriteTo(packet[3:], pc.rAddr) +} + +func (pc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, _, e := pc.PacketConn.ReadFrom(b) + if e != nil { + return 0, nil, e + } + + addr := socks5.SplitAddr(b[:n]) + if addr == nil { + return 0, nil, errors.New("parse addr error") + } + + udpAddr := addr.UDPAddr() + if udpAddr == nil { + return 0, nil, errors.New("parse addr error") + } + + copy(b, b[len(addr):]) + return n - len(addr), udpAddr, e +} diff --git a/internal/proxy/socks5.go b/internal/proxy/socks5.go new file mode 100755 index 0000000..b815229 --- /dev/null +++ b/internal/proxy/socks5.go @@ -0,0 +1,160 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net" + "net/url" + + "github.com/xjasonlyu/clash/component/dialer" + "github.com/xjasonlyu/clash/component/socks5" + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +type Socks5 struct { + *Base + + user string + pass string +} + +func NewSocks5(url *url.URL, user, pass string) (*Socks5, error) { + return &Socks5{ + Base: &Base{ + url: url, + }, + user: user, + pass: pass, + }, nil +} + +func (ss *Socks5) DialContext(ctx context.Context, metadata *adapter.Metadata) (c net.Conn, err error) { + c, err = dialer.DialContext(ctx, "tcp", ss.Addr()) + if err != nil { + return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err) + } + tcpKeepAlive(c) + + defer func() { + if err != nil { + c.Close() + } + }() + + var user *socks5.User + if ss.user != "" { + user = &socks5.User{ + Username: ss.user, + Password: ss.pass, + } + } + + if _, err := socks5.ClientHandshake(c, metadata.SerializesSocksAddr(), socks5.CmdConnect, user); err != nil { + return nil, fmt.Errorf("client hanshake: %w", err) + } + + return +} + +func (ss *Socks5) DialUDP(_ *adapter.Metadata) (_ net.PacketConn, err error) { + ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout) + defer cancel() + c, err := dialer.DialContext(ctx, "tcp", ss.Addr()) + if err != nil { + err = fmt.Errorf("connect to %s: %w", ss.Addr(), err) + return + } + tcpKeepAlive(c) + + defer func() { + if err != nil { + c.Close() + } + }() + + var user *socks5.User + if ss.user != "" { + user = &socks5.User{ + Username: ss.user, + Password: ss.pass, + } + } + + // The UDP ASSOCIATE request is used to establish an association within + // the UDP relay process to handle UDP datagrams. The DST.ADDR and + // DST.PORT fields contain the address and port that the client expects + // to use to send UDP datagrams on for the association. The server MAY + // use this information to limit access to the association. If the + // client is not in possession of the information at the time of the UDP + // ASSOCIATE, the client MUST use a port number and address of all + // zeros. RFC1928 + var targetAddr socks5.Addr = []byte{socks5.AtypIPv4, 0, 0, 0, 0, 0, 0} + + bindAddr, err := socks5.ClientHandshake(c, targetAddr, socks5.CmdUDPAssociate, user) + if err != nil { + return nil, fmt.Errorf("client hanshake: %w", err) + } + + pc, err := dialer.ListenPacket("udp", "") + if err != nil { + return + } + + go func() { + io.Copy(ioutil.Discard, c) + c.Close() + // A UDP association terminates when the TCP connection that the UDP + // ASSOCIATE request arrived on terminates. RFC1928 + pc.Close() + }() + + return &socksPacketConn{PacketConn: pc, rAddr: bindAddr.UDPAddr(), tcpConn: c}, nil +} + +type socksPacketConn struct { + net.PacketConn + + rAddr net.Addr + tcpConn net.Conn +} + +func (pc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + var packet []byte + if m, ok := addr.(*adapter.Metadata); ok { + packet, err = socks5.EncodeUDPPacket(m.SerializesSocksAddr(), b) + } else { + packet, err = socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b) + } + + if err != nil { + return + } + return pc.PacketConn.WriteTo(packet, pc.rAddr) +} + +func (pc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, _, e := pc.PacketConn.ReadFrom(b) + if e != nil { + return 0, nil, e + } + addr, payload, err := socks5.DecodeUDPPacket(b) + if err != nil { + return 0, nil, err + } + + udpAddr := addr.UDPAddr() + if udpAddr == nil { + return 0, nil, fmt.Errorf("convert %s to UDPAddr failed", addr) + } + + // due to DecodeUDPPacket is mutable, record addr length + copy(b, payload) + return n - len(addr) - 3, udpAddr, nil +} + +func (pc *socksPacketConn) Close() error { + pc.tcpConn.Close() + return pc.PacketConn.Close() +} diff --git a/internal/proxy/utils.go b/internal/proxy/utils.go new file mode 100755 index 0000000..5173ba2 --- /dev/null +++ b/internal/proxy/utils.go @@ -0,0 +1,33 @@ +package proxy + +import ( + "net" + "time" + + "github.com/xjasonlyu/clash/component/resolver" +) + +const ( + tcpConnectTimeout = 5 * time.Second + tcpKeepAlivePeriod = 30 * time.Second +) + +func tcpKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetKeepAlivePeriod(tcpKeepAlivePeriod) + } +} + +func resolveUDPAddr(network, address string) (*net.UDPAddr, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + ip, err := resolver.ResolveIP(host) + if err != nil { + return nil, err + } + return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) +} diff --git a/internal/tunnel/tcp.go b/internal/tunnel/tcp.go new file mode 100644 index 0000000..539eb4d --- /dev/null +++ b/internal/tunnel/tcp.go @@ -0,0 +1,86 @@ +package tunnel + +import ( + "io" + "net" + "strconv" + "sync" + "time" + + "github.com/xjasonlyu/clash/common/pool" + "github.com/xjasonlyu/tun2socks/internal/adapter" + "github.com/xjasonlyu/tun2socks/internal/manager" + "github.com/xjasonlyu/tun2socks/internal/proxy" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +const ( + tcpWaitTimeout = 5 * time.Second + relayBufferSize = pool.RelayBufferSize +) + +func handleTCP(localConn adapter.TCPConn) { + defer localConn.Close() + + metadata := localConn.Metadata() + if !metadata.Valid() { + log.Warnf("[Metadata] not valid: %#v", metadata) + return + } + + err := resolveMetadata(metadata) + if err != nil { + log.Warnf("[Metadata] resolve metadata error: %v", err) + return + } + + targetConn, err := proxy.Dial(metadata) + if err != nil { + log.Warnf("[TCP] dial %s error: %v", metadata.DestinationAddress(), err) + return + } + + if dialerAddr, ok := targetConn.LocalAddr().(*net.TCPAddr); ok { + metadata.MidIP = dialerAddr.IP + metadata.MidPort = uint16(dialerAddr.Port) + } else { + ip, p, _ := net.SplitHostPort(targetConn.LocalAddr().String()) + port, _ := strconv.ParseUint(p, 10, 16) + metadata.MidIP = net.ParseIP(ip) + metadata.MidPort = uint16(port) + } + + targetConn = manager.NewTCPTracker(targetConn, metadata) + defer targetConn.Close() + + log.Infof("[TCP] %s <--> %s", metadata.SourceAddress(), metadata.DestinationAddress()) + relay(localConn, targetConn) /* relay connections */ +} + +// relay copies between left and right bidirectionally. +func relay(left, right net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + _ = copyBuffer(right, left) /* ignore error */ + right.SetReadDeadline(time.Now().Add(tcpWaitTimeout)) + }() + + go func() { + defer wg.Done() + _ = copyBuffer(left, right) /* ignore error */ + left.SetReadDeadline(time.Now().Add(tcpWaitTimeout)) + }() + + wg.Wait() +} + +func copyBuffer(dst io.Writer, src io.Reader) error { + buf := pool.Get(relayBufferSize) + defer pool.Put(buf) + + _, err := io.CopyBuffer(dst, src, buf) + return err +} diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go new file mode 100644 index 0000000..69ae52d --- /dev/null +++ b/internal/tunnel/tunnel.go @@ -0,0 +1,67 @@ +package tunnel + +import ( + "runtime" + + "github.com/xjasonlyu/tun2socks/internal/adapter" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +const ( + // maxUDPQueueSize is the max number of UDP packets + // could be buffered. if queue is full, upcoming packets + // would be dropped util queue is ready again. + maxUDPQueueSize = 2 << 10 +) + +var ( + numUDPWorkers = max(runtime.NumCPU(), 4 /* at least 4 workers */) + + tcpQueue = make(chan adapter.TCPConn) /* unbuffered */ + udpMultiQueue = make([]chan adapter.UDPPacket, 0, numUDPWorkers) +) + +func init() { + for i := 0; i < numUDPWorkers; i++ { + udpMultiQueue = append(udpMultiQueue, make(chan adapter.UDPPacket, maxUDPQueueSize)) + } + + go process() +} + +// Add adds tcpConn to tcpQueue. +func Add(conn adapter.TCPConn) { + tcpQueue <- conn +} + +// AddPacket adds udpPacket to udpQueue. +func AddPacket(packet adapter.UDPPacket) { + m := packet.Metadata() + // In order to keep each packet sent in order, we + // calculate which queue each packet should be sent + // by src/dst info, and make sure the rest of them + // would only be sent to the same queue. + i := int(m.SrcPort+m.DstPort) % numUDPWorkers + + select { + case udpMultiQueue[i] <- packet: + default: + log.Warnf("queue is currently full, packet will be dropped") + packet.Drop() + } +} + +func process() { + for _, udpQueue := range udpMultiQueue { + queue := udpQueue + go func() { + for packet := range queue { + handleUDP(packet) + } + }() + } + + for conn := range tcpQueue { + go handleTCP(conn) + } +} diff --git a/internal/tunnel/udp.go b/internal/tunnel/udp.go new file mode 100644 index 0000000..9396ad3 --- /dev/null +++ b/internal/tunnel/udp.go @@ -0,0 +1,150 @@ +package tunnel + +import ( + "errors" + "net" + "os" + "strconv" + "time" + + "github.com/xjasonlyu/clash/common/pool" + "github.com/xjasonlyu/clash/component/resolver" + "github.com/xjasonlyu/tun2socks/internal/adapter" + "github.com/xjasonlyu/tun2socks/internal/manager" + "github.com/xjasonlyu/tun2socks/internal/proxy" + "github.com/xjasonlyu/tun2socks/pkg/log" + "github.com/xjasonlyu/tun2socks/pkg/nat" +) + +const ( + udpTimeout = 30 * time.Second + udpBufferSize = 64 << 10 // KiB +) + +var ( + // natTable uses source udp packet information + // as key to store destination udp packetConn. + natTable = nat.NewTable() +) + +func handleUDP(packet adapter.UDPPacket) { + metadata := packet.Metadata() + if !metadata.Valid() { + log.Warnf("[Metadata] not valid: %#v", metadata) + return + } + + // make a fAddr if request ip is fake ip. + var fAddr net.Addr + if resolver.IsExistFakeIP(metadata.DstIP) { + fAddr = metadata.UDPAddr() + } + + err := resolveMetadata(metadata) + if err != nil { + log.Warnf("[Metadata] resolve metadata error: %v", err) + return + } + + key := generateNATKey(metadata) + + handle := func(drop bool) bool { + pc := natTable.Get(key) + if pc != nil { + handleUDPToRemote(packet, pc, metadata /* as net.Addr */, drop) + return true + } + return false + } + + if handle(true /* drop */) { + return + } + + lockKey := key + "-lock" + cond, loaded := natTable.GetOrCreateLock(lockKey) + go func() { + if loaded { + cond.L.Lock() + cond.Wait() + handle(true) /* drop after sending data to remote */ + cond.L.Unlock() + return + } + + defer func() { + natTable.Delete(lockKey) + cond.Broadcast() + }() + + pc, err := proxy.DialUDP(metadata) + if err != nil { + log.Warnf("[UDP] dial %s error: %v", metadata.DestinationAddress(), err) + return + } + + if dialerAddr, ok := pc.LocalAddr().(*net.UDPAddr); ok { + metadata.MidIP = dialerAddr.IP + metadata.MidPort = uint16(dialerAddr.Port) + } else { + ip, p, _ := net.SplitHostPort(pc.LocalAddr().String()) + port, _ := strconv.ParseUint(p, 10, 16) + metadata.MidIP = net.ParseIP(ip) + metadata.MidPort = uint16(port) + } + + pc = manager.NewUDPTracker(pc, metadata) + + go func() { + defer pc.Close() + defer packet.Drop() + defer natTable.Delete(key) + + handleUDPToLocal(packet, pc, fAddr, udpTimeout) + }() + + natTable.Set(key, pc) + handle(false /* drop */) + }() +} + +func handleUDPToRemote(packet adapter.UDPPacket, pc net.PacketConn, remote net.Addr, drop bool) { + defer func() { + if drop { + packet.Drop() + } + }() + + if _, err := pc.WriteTo(packet.Data() /* data */, remote); err != nil { + log.Warnf("[UDP] write to %s error: %v", remote, err) + } + + log.Infof("[UDP] %s --> %s", packet.RemoteAddr(), remote) +} + +func handleUDPToLocal(packet adapter.UDPPacket, pc net.PacketConn, fAddr net.Addr, timeout time.Duration) { + buf := pool.Get(udpBufferSize) + defer pool.Put(buf) + + for /* just loop */ { + pc.SetReadDeadline(time.Now().Add(timeout)) + n, from, err := pc.ReadFrom(buf) + if err != nil { + if !errors.Is(err, os.ErrDeadlineExceeded) /* ignore i/o timeout */ { + log.Warnf("[UDP] ReadFrom error: %v", err) + } + return + } + + if fAddr != nil { + from = fAddr + } + + if _, err := packet.WriteBack(buf[:n], from); err != nil { + log.Warnf("[UDP] write back from %s error: %v", from, err) + return + } + + log.Infof("[UDP] %s <-- %s", packet.RemoteAddr(), from) + } +} diff --git a/internal/tunnel/utils.go b/internal/tunnel/utils.go new file mode 100644 index 0000000..dffbb58 --- /dev/null +++ b/internal/tunnel/utils.go @@ -0,0 +1,35 @@ +package tunnel + +import ( + "fmt" + + "github.com/xjasonlyu/clash/component/resolver" + "github.com/xjasonlyu/tun2socks/internal/adapter" +) + +func generateNATKey(m *adapter.Metadata) string { + return m.SourceAddress() /* Full Cone NAT Key */ +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func resolveMetadata(metadata *adapter.Metadata) error { + if metadata.DstIP == nil { + return fmt.Errorf("destination IP is nil") + } + + if resolver.IsFakeIP(metadata.DstIP) { + var exist bool + metadata.Host, exist = resolver.FindHostByIP(metadata.DstIP) + if !exist { + return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) + } + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f8f5b03 --- /dev/null +++ b/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + "time" + + "github.com/urfave/cli/v2" + + "github.com/xjasonlyu/tun2socks/internal/cmd" + "github.com/xjasonlyu/tun2socks/pkg/log" +) + +const Usage = "A tun2socks implementation written in Go." + +var ( + Version string + BuildTime string +) + +func main() { + app := &cli.App{ + Usage: Usage, + Version: Version, + Action: cmd.Main, + Flags: []cli.Flag{ + &cmd.API, + &cmd.Device, + &cmd.DNS, + &cmd.Hosts, + &cmd.Interface, + &cmd.LogLevel, + &cmd.Proxy, + &cmd.Version, + }, + HideVersion: true, + HideHelpCommand: true, + } + app.Compiled, _ = time.Parse(time.RFC3339, BuildTime) + + if err := app.Run(os.Args); err != nil { + log.Fatalf("Failed to start: %v", err) + } +} diff --git a/pkg/link/rwc/endpoint.go b/pkg/link/rwc/endpoint.go new file mode 100644 index 0000000..b634f06 --- /dev/null +++ b/pkg/link/rwc/endpoint.go @@ -0,0 +1,169 @@ +package rwc + +import ( + "errors" + "io" + "sync" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/buffer" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// Endpoint wraps io.ReadWriter to stack.LinkEndpoint. +type Endpoint struct { + mtu uint32 + rwc io.ReadWriteCloser + + wg sync.WaitGroup + + dispatcher stack.NetworkDispatcher + LinkEPCapabilities stack.LinkEndpointCapabilities +} + +// New returns stack.LinkEndpoint(.*Endpoint) and error. +func New(rwc io.ReadWriteCloser, mtu uint32) (*Endpoint, error) { + switch { + case mtu == 0: + return nil, errors.New("MTU size is zero") + case rwc == nil: + return nil, errors.New("RWC interface is nil") + } + + return &Endpoint{ + rwc: rwc, + mtu: mtu, + }, nil +} + +// Attach launches the goroutine that reads packets from io.ReadWriter and +// dispatches them via the provided dispatcher. +func (e *Endpoint) Attach(dispatcher stack.NetworkDispatcher) { + go e.dispatchLoop() + e.dispatcher = dispatcher +} + +// IsAttached implements stack.LinkEndpoint.IsAttached. +func (e *Endpoint) IsAttached() bool { + return e.dispatcher != nil +} + +// dispatchLoop dispatches packets to upper layer. +func (e *Endpoint) dispatchLoop() { + e.wg.Add(1) + defer e.wg.Done() + + for { + packet := make([]byte, e.mtu) + n, err := e.rwc.Read(packet) + if err != nil { + break + } + + if !e.IsAttached() { + continue + } + + var p tcpip.NetworkProtocolNumber + switch header.IPVersion(packet) { + case header.IPv4Version: + p = header.IPv4ProtocolNumber + case header.IPv6Version: + p = header.IPv6ProtocolNumber + } + + e.dispatcher.DeliverNetworkPacket("", "", p, &stack.PacketBuffer{ + Data: buffer.View(packet[:n]).ToVectorisedView(), + }) + } +} + +// writePacket writes packets back into io.ReadWriter. +func (e *Endpoint) writePacket(pkt *stack.PacketBuffer) *tcpip.Error { + networkHdr := pkt.NetworkHeader().View() + transportHdr := pkt.TransportHeader().View() + payload := pkt.Data.ToView() + + buf := buffer.NewVectorisedView( + len(networkHdr)+len(transportHdr)+len(payload), + []buffer.View{networkHdr, transportHdr, payload}, + ) + + if _, err := e.rwc.Write(buf.ToView()); err != nil { + return tcpip.ErrInvalidEndpointState + } + + return nil +} + +// WritePacket writes packets back into io.ReadWriter. +func (e *Endpoint) WritePacket(_ *stack.Route, _ *stack.GSO, _ tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) *tcpip.Error { + return e.writePacket(pkt) +} + +// WritePackets writes packets back into io.ReadWriter. +func (e *Endpoint) WritePackets(_ *stack.Route, _ *stack.GSO, pkts stack.PacketBufferList, _ tcpip.NetworkProtocolNumber) (int, *tcpip.Error) { + n := 0 + for pkt := pkts.Front(); pkt != nil; pkt = pkt.Next() { + if err := e.writePacket(pkt); err != nil { + break + } + n++ + } + return n, nil +} + +// WriteRawPacket implements stack.LinkEndpoint.WriteRawPacket. +func (e *Endpoint) WriteRawPacket(vv buffer.VectorisedView) *tcpip.Error { + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Data: vv, + }) + return e.writePacket(pkt) +} + +// MTU implements stack.LinkEndpoint.MTU. +func (e *Endpoint) MTU() uint32 { + return e.mtu +} + +// Capabilities implements stack.LinkEndpoint.Capabilities. +func (e *Endpoint) Capabilities() stack.LinkEndpointCapabilities { + return e.LinkEPCapabilities +} + +// GSOMaxSize returns the maximum GSO packet size. +func (*Endpoint) GSOMaxSize() uint32 { + return 1 << 15 +} + +// MaxHeaderLength returns the maximum size of the link layer header. Given it +// doesn't have a header, it just returns 0. +func (*Endpoint) MaxHeaderLength() uint16 { + return 0 +} + +// LinkAddress returns the link address of this endpoint. +func (*Endpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +// ARPHardwareType implements stack.LinkEndpoint.ARPHardwareType. +func (*Endpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +// AddHeader implements stack.LinkEndpoint.AddHeader. +func (e *Endpoint) AddHeader(tcpip.LinkAddress, tcpip.LinkAddress, tcpip.NetworkProtocolNumber, *stack.PacketBuffer) { +} + +// Wait implements stack.LinkEndpoint.Wait. +func (e *Endpoint) Wait() { + e.wg.Wait() +} + +// Close closes io.ReadWriteCloser and set closed to true. +func (e *Endpoint) Close() error { + e.rwc.Close() + return nil +} diff --git a/pkg/log/event.go b/pkg/log/event.go new file mode 100644 index 0000000..c250408 --- /dev/null +++ b/pkg/log/event.go @@ -0,0 +1,39 @@ +package log + +import ( + "fmt" + "time" + + "github.com/xjasonlyu/clash/common/observable" +) + +var ( + logCh = make(chan interface{}) + source = observable.NewObservable(logCh) +) + +type Event struct { + Level Level `json:"level"` + Message string `json:"msg"` + Time time.Time `json:"time"` +} + +func newEvent(level Level, format string, args ...interface{}) *Event { + event := &Event{ + Level: level, + Time: time.Now(), + Message: fmt.Sprintf(format, args...), + } + logCh <- event /* send all events to logCh */ + + return event +} + +func Subscribe() observable.Subscription { + sub, _ := source.Subscribe() + return sub +} + +func UnSubscribe(sub observable.Subscription) { + source.UnSubscribe(sub) +} diff --git a/pkg/log/level.go b/pkg/log/level.go new file mode 100644 index 0000000..b0a6d52 --- /dev/null +++ b/pkg/log/level.go @@ -0,0 +1,77 @@ +package log + +import ( + "encoding/json" + "fmt" + "strings" +) + +type Level uint32 + +const ( + SilentLevel Level = iota + FatalLevel + ErrorLevel + WarnLevel + InfoLevel + DebugLevel +) + +// UnmarshalJSON deserialize Level with json +func (level *Level) UnmarshalJSON(data []byte) error { + var lvl string + if err := json.Unmarshal(data, &lvl); err != nil { + return err + } + + l, err := ParseLevel(lvl) + if err != nil { + return err + } + + *level = l + return nil +} + +// MarshalJSON serialize Level with json +func (level Level) MarshalJSON() ([]byte, error) { + return json.Marshal(level.String()) +} + +func (level Level) String() string { + switch level { + case DebugLevel: + return "debug" + case InfoLevel: + return "info" + case WarnLevel: + return "warning" + case ErrorLevel: + return "error" + case FatalLevel: + return "fatal" + case SilentLevel: + return "silent" + default: + return fmt.Sprintf("not a valid level %d", level) + } +} + +func ParseLevel(lvl string) (Level, error) { + switch strings.ToLower(lvl) { + case "silent", "none": + return SilentLevel, nil + case "fatal": + return FatalLevel, nil + case "error": + return ErrorLevel, nil + case "warn", "warning": + return WarnLevel, nil + case "info": + return InfoLevel, nil + case "debug": + return DebugLevel, nil + default: + return Level(0), fmt.Errorf("not a valid logrus Level: %q", lvl) + } +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..7e541d7 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,60 @@ +package log + +import ( + "os" + "sync/atomic" + + "github.com/sirupsen/logrus" +) + +var ( + // defaultLevel is package default loglevel. + defaultLevel = InfoLevel +) + +func init() { + logrus.SetOutput(os.Stdout) + logrus.SetLevel(logrus.DebugLevel) +} + +func SetLevel(level Level) { + atomic.StoreUint32((*uint32)(&defaultLevel), uint32(level)) +} + +func Debugf(format string, args ...interface{}) { + logf(DebugLevel, format, args...) +} + +func Infof(format string, args ...interface{}) { + logf(InfoLevel, format, args...) +} + +func Warnf(format string, args ...interface{}) { + logf(WarnLevel, format, args...) +} + +func Errorf(format string, args ...interface{}) { + logf(ErrorLevel, format, args...) +} + +func Fatalf(format string, args ...interface{}) { + logrus.Fatalf(format, args...) +} + +func logf(level Level, format string, args ...interface{}) { + event := newEvent(level, format, args...) + if event.Level > defaultLevel { + return + } + + switch level { + case DebugLevel: + logrus.WithTime(event.Time).Debugln(event.Message) + case InfoLevel: + logrus.WithTime(event.Time).Infoln(event.Message) + case WarnLevel: + logrus.WithTime(event.Time).Warnln(event.Message) + case ErrorLevel: + logrus.WithTime(event.Time).Errorln(event.Message) + } +} diff --git a/pkg/nat/doc.go b/pkg/nat/doc.go new file mode 100644 index 0000000..15587e0 --- /dev/null +++ b/pkg/nat/doc.go @@ -0,0 +1,31 @@ +/* +Package nat provides simple NAT table implements. + +Normal (Full Cone) NAT +A full cone NAT is one where all requests from the same internal IP address +and port are mapped to the same external IP address and port. Furthermore, +any external host can send a packet to the internal host, by sending a packet +to the mapped external address. + +Restricted Cone NAT +A restricted cone NAT is one where all requests from the same internal IP +address and port are mapped to the same external IP address and port. +Unlike a full cone NAT, an external host (with IP address X) can send a +packet to the internal host only if the internal host had previously sent +a packet to IP address X. + +Port Restricted Cone NAT +A port restricted cone NAT is like a restricted cone NAT, but the restriction +includes port numbers. Specifically, an external host can send a packet, with +source IP address X and source port P, to the internal host only if the internal +host had previously sent a packet to IP address X and port P. + +Symmetric NAT +A symmetric NAT is one where all requests from the same internal IP address +and port, to a specific destination IP address and port, are mapped to the +same external IP address and port. If the same host sends a packet with the +same source address and port, but to a different destination, a different mapping +is used. Furthermore, only the external host that receives a packet can send a +UDP packet back to the internal host. +*/ +package nat diff --git a/pkg/nat/table.go b/pkg/nat/table.go new file mode 100644 index 0000000..645773f --- /dev/null +++ b/pkg/nat/table.go @@ -0,0 +1,35 @@ +package nat + +import ( + "net" + "sync" +) + +type Table struct { + mapping sync.Map +} + +func (t *Table) Set(key string, pc net.PacketConn) { + t.mapping.Store(key, pc) +} + +func (t *Table) Get(key string) net.PacketConn { + item, exist := t.mapping.Load(key) + if !exist { + return nil + } + return item.(net.PacketConn) +} + +func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) { + item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{})) + return item.(*sync.Cond), loaded +} + +func (t *Table) Delete(key string) { + t.mapping.Delete(key) +} + +func NewTable() *Table { + return &Table{} +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..9e4f29f --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +TUN="${TUN:-tun0}" +ETH="${ETH:-eth0}" +TUN_ADDR="${TUN_ADDR:-198.18.0.1}" +TUN_MASK="${TUN_MASK:-255.254.0.0}" +LOGLEVEL="${LOGLEVEL:-INFO}" + +mk_tun() { + # params + NAME="$1" + ADDR="$2" + MASK="$3" + # create tun device + ip tuntap add mode tun dev "$NAME" + ip addr add "$ADDR/$MASK" dev "$NAME" + ip link set dev "$NAME" up +} + +config_route() { + # params + TABLE="$1" + TUN_IF="$2" + ETH_IF="$3" + TUN_EXCLUDED="$4" + + # add custom table + printf "%s\t%s\n" 100 "$TABLE" >>/etc/iproute2/rt_tables + + # clone main route + ip route show table main | + while read -r route; do + ip route add $route table "$TABLE" + done + + # config default route + ip route del default table "$TABLE" + ip route add default dev "$TUN_IF" table "$TABLE" + + # policy routing + tun=$(ip -4 addr show "$TUN_IF" | awk 'NR==2 {print $2}') + eth=$(ip -4 addr show "$ETH_IF" | awk 'NR==2 {split($2,a,"/");print a[1]}') + ip rule add from "$eth" to "$tun" priority 1000 prohibit + ip rule add from "$eth" priority 2000 table main + ip rule add from all priority 3000 table "$TABLE" + + # add tun excluded to route + for addr in $(echo "$TUN_EXCLUDED" | tr ',' '\n'); do + ip rule add to "$addr" table main + done +} + +main() { + mk_tun "$TUN" "$TUN_ADDR" "$TUN_MASK" + config_route "tun2socks" "$TUN" "$ETH" "$EXCLUDED" + + # execute extra commands + if [ -n "$EXTRACMD" ]; then + sh -c "$EXTRACMD" + fi + + if [ -n "$API" ]; then + ARGS="--api $API" + fi + + if [ -n "$DNS" ]; then + ARGS="$ARGS --dns $DNS" + fi + + for item in $(echo "$HOSTS" | tr ',' '\n'); do + ARGS="$ARGS --hosts $item" + done + + exec tun2socks \ + --loglevel "$LOGLEVEL" \ + --interface "$ETH" \ + --device "$TUN" \ + --proxy "$PROXY" \ + $ARGS +} + +main || exit 1 diff --git a/scripts/load_tun.sh b/scripts/load_tun.sh new file mode 100644 index 0000000..d76c5d4 --- /dev/null +++ b/scripts/load_tun.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Create the necessary file structure for /dev/net/tun +if [ ! -c /dev/net/tun ]; then + if [ ! -d /dev/net ]; then + mkdir -m 755 /dev/net + fi + mknod /dev/net/tun c 10 200 + chmod 0755 /dev/net/tun +fi + +# Load the tun module if not already loaded +if ( ! (lsmod | grep -q "^tun\s")); then + insmod /lib/modules/tun.ko +fi