diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..aee86f4 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,108 @@ +name: build + +run-name: ${{ gitea.actor }} build Transocks + +on: + push: + branches: + - "**" + tags-ignore: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: check-out + uses: https://gitea.com/actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: set-up go + uses: https://gitea.com/actions/setup-go@v3 + with: + go-version: ">=1.22" + + - name: set-up dependencies + run: | + go install github.com/kisielk/errcheck@latest + go install honnef.co/go/tools/cmd/staticcheck@latest + go install github.com/sashamelentyev/usestdlibvars@latest + go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest + make vendor + + - name: lint + run: make lint + + - name: golangci-lint + uses: https://github.com/golangci/golangci-lint-action@v6 + with: + version: v1.60 + + - name: build amd64 + id: build-amd + run: | + echo "ARTIFACT=transocks-$(make version)_$(go env GOOS)-amd64" >> $GITHUB_OUTPUT + GOARCH=amd64 make clean build + + - name: upload amd64 + uses: https://gitea.com/actions/upload-artifact@v3 + with: + name: ${{ steps.build-amd.outputs.ARTIFACT }} + path: out/bin/* + overwrite: true + + - name: build arm64 + id: build-arm + run: | + echo "ARTIFACT=transocks-$(make version)_$(go env GOOS)-arm64" >> $GITHUB_OUTPUT + GOARCH=arm64 make clean build + + - name: upload arm64 + uses: https://gitea.com/actions/upload-artifact@v3 + with: + name: ${{ steps.build-arm.outputs.ARTIFACT }} + path: out/bin/* + overwrite: true + + build_windows: + runs-on: windows + defaults: + run: + shell: bash + steps: + - name: check-out + uses: https://gitea.com/actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: set-up dependencies + run: make vendor + + - name: build amd + id: build-amd + run: | + echo "ARTIFACT=transocks-$(make version)_$(go env GOOS)-amd64" >> $GITHUB_OUTPUT + GOARCH=amd64 make build + + - name: upload amd + uses: https://gitea.com/actions/upload-artifact@v3 + with: + name: ${{ steps.build-amd.outputs.ARTIFACT }} + path: out/bin/* + overwrite: true + + - name: build arm + id: build-arm + run: | + echo "ARTIFACT=transocks-$(make version)_$(go env GOOS)-arm64" >> $GITHUB_OUTPUT + GOARCH=arm64 make build + + - name: upload amd + uses: https://gitea.com/actions/upload-artifact@v3 + with: + name: ${{ steps.build-arm.outputs.ARTIFACT }} + path: out/bin/* + overwrite: true diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 79ad262..91162c3 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -10,7 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: check-out - uses: actions/checkout@v4 + uses: https://gitea.com/actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - name: set-up qemu uses: https://git.mousesoft.ru/ms/gitea-action-setup-qemu@v3 diff --git a/.golangci.yml b/.golangci.yml index a0e23db..34193c1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,6 +3,5 @@ linters: - dupl - goconst - gofmt - - golint - typecheck - unparam diff --git a/config.go b/config.go index 62e8aea..90055c3 100644 --- a/config.go +++ b/config.go @@ -75,10 +75,10 @@ func NewConfig() *Config { // It returns non-nil error if the configuration is not valid. func (c *Config) validate() error { if c.ProxyURL == nil { - return errors.New("ProxyURL is nil") + return errors.New("proxy URL is nil") } if c.Mode != ModeNAT { - return fmt.Errorf("Unknown mode: %s", c.Mode) + return fmt.Errorf("unknown mode: %s", c.Mode) } return nil } diff --git a/http_tunnel.go b/http_tunnel.go index 0830c1f..79a0c59 100644 --- a/http_tunnel.go +++ b/http_tunnel.go @@ -12,7 +12,6 @@ import ( "bufio" "bytes" "encoding/base64" - "errors" "fmt" "net" "net/http" @@ -51,7 +50,7 @@ func httpDialType(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { func (d *httpDialer) Dial(network, addr string) (c net.Conn, err error) { req := &http.Request{ - Method: "CONNECT", + Method: http.MethodConnect, URL: &url.URL{Opaque: addr}, Host: addr, Header: d.header, @@ -60,20 +59,24 @@ func (d *httpDialer) Dial(network, addr string) (c net.Conn, err error) { if err != nil { return } - req.Write(c) + if err = req.Write(c); err != nil { + return + } // Read response until "\r\n\r\n". // bufio cannot be used as the connected server may not be // a HTTP(S) server. - c.SetReadDeadline(time.Now().Add(10 * time.Second)) + if err = c.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + return + } + buf := make([]byte, 0, 4096) b := make([]byte, 1) state := 0 for { - _, e := c.Read(b) - if e != nil { - c.Close() - return nil, errors.New("reset proxy connection") + if _, err = c.Read(b); err != nil { + _ = c.Close() + return nil, fmt.Errorf("reset proxy connection: %w", err) } buf = append(buf, b[0]) switch state { @@ -107,15 +110,21 @@ func (d *httpDialer) Dial(network, addr string) (c net.Conn, err error) { PARSE: var zero time.Time - c.SetReadDeadline(zero) - resp, e := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(buf)), req) - if e != nil { - c.Close() - return nil, e + + if err = c.SetReadDeadline(zero); err != nil { + return nil, err } - resp.Body.Close() - if resp.StatusCode != 200 { - c.Close() + + resp, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(buf)), req) + if err != nil { + _ = c.Close() + return nil, err + } + + _ = resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + _ = c.Close() return nil, fmt.Errorf("proxy returns %s", resp.Status) } diff --git a/http_tunnel_test.go b/http_tunnel_test.go index bfedd0f..d5f3b71 100644 --- a/http_tunnel_test.go +++ b/http_tunnel_test.go @@ -23,8 +23,9 @@ func TestHTTPDialer(t *testing.T) { if err != nil { t.Fatal(err) } - defer conn.Close() - conn.Write([]byte("GET / HTTP/1.1\r\nHost: www.yahoo.com:80\r\nConnection: close\r\n\r\n")) - io.Copy(os.Stdout, conn) + defer func() { _ = conn.Close() }() + + _, _ = conn.Write([]byte("GET / HTTP/1.1\r\nHost: www.yahoo.com:80\r\nConnection: close\r\n\r\n")) + _, _ = io.Copy(os.Stdout, conn) } diff --git a/makefile b/makefile new file mode 100644 index 0000000..a0a75d5 --- /dev/null +++ b/makefile @@ -0,0 +1,138 @@ +# transocks makefile +# ================== + +SHELL := /usr/bin/env bash + +PROJECT_ID := transocks +PROJECT_NAME ?= Transocks +BIN_SUFFIX := + +TMPDIR ?= $(CURDIR)/tmp +OUTDIR ?= $(CURDIR)/out +BINDIR ?= $(OUTDIR)/bin + +VERSION ?= $(strip $(shell ./scripts/version.sh)) +VERSION_NUMBER := $(strip $(shell ./scripts/version.sh number)) + +BUILD_OS ?= $(shell go env GOOS) +BUILD_ARCH ?= $(shell go env GOARCH) + +GO_LDFLAGS ?= +GO_OPT_V := -X "main.version=$(VERSION)" +GO_OPT_APP := -X "main.appname=$(PROJECT_NAME)" +GO_OPT_BASE := -ldflags '$(GO_OPT_V) $(GO_OPT_APP) $(GO_LDFLAGS)' +GO_OPT ?= +EXPORT_RESULT ?= false # for CI please set EXPORT_RESULT to true + +GOCMD := go +GOTEST := $(GOCMD) test +GOVET := $(GOCMD) vet +ECHO_CMD := echo -e + +ifeq ($(OS),Windows_NT) +MSI_FILE ?= $(PROJECT_ID)_$(VERSION)_$(BUILD_ARCH).msi +DIST_EXT := .zip +DIST_OPTS := -a -cf +MSI_VERSION := $(shell echo $(VERSION_NUMBER) | sed -e 's/-.*//') +BIN_SUFFIX := .exe +else +PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]') +DIST_EXT := .tar.gz +DIST_OPTS := -czf +endif + +PKG_NAME := $(PROJECT_ID)_$(VERSION)_$(BUILD_OS)-$(BUILD_ARCH) +DIST_FILE := $(PKG_NAME)$(DIST_EXT) + +GREEN := $(shell tput -Txterm setaf 2) +YELLOW := $(shell tput -Txterm setaf 3) +WHITE := $(shell tput -Txterm setaf 7) +CYAN := $(shell tput -Txterm setaf 6) +RESET := $(shell tput -Txterm sgr0) + +.DEFAULT_GOAL := all + +version: ## Version of the project to be built + @echo $(VERSION) +.PHONY:version + +version-number: ## Version number of the project to be built + @echo $(VERSION_NUMBER) +.PHONY:version-number + +## Build + +all: clean vendor build ## Build binary +.PHONY:all + +APPS := $(patsubst cmd/%/, %, $(dir $(wildcard cmd/*/main.go))) + +build: $(addprefix $(BINDIR)/, $(APPS)) ## Build your project and put the output binary in out/bin/ + @mkdir -p $(BINDIR) + @$(ECHO_CMD) "Build\t\t${GREEN}[OK]${RESET}" +.PHONY:build + +$(BINDIR)/%: cmd/%/main.go $(patsubst cmd/%/main.go,cmd/%/*.go,$<) + @rm -f "$(BINDIR)/$(BIN_PREFIX)$(patsubst cmd/%/main.go,%,$<)$(BIN_SUFFIX)" + $(GOCMD) build $(GO_OPT_BASE) $(GO_OPT) \ + -o "$(BINDIR)/$(BIN_PREFIX)$(patsubst cmd/%/main.go,%,$<)$(BIN_SUFFIX)" \ + $(patsubst %/main.go,./%,$<) + +dist: ## Create all distro packages + @rm -f "$(OUTDIR)/$(DIST_FILE)" + tar $(DIST_OPTS) "$(OUTDIR)/$(DIST_FILE)" -C "$(OUTDIR)" bin + @$(ECHO_CMD) "Dist\t\t${GREEN}[OK]${RESET}" +.PHONY:dist + +vendor: ## Copy of all packages needed to support builds and tests in the vendor directory + $(GOCMD) mod vendor + @$(ECHO_CMD) "Vendor\t\t${GREEN}[OK]${RESET}" +.PHONY:vendor + +clean: ## Remove build related files + @rm -fr $(TMPDIR) + @rm -fr $(OUTDIR) + @$(ECHO_CMD) "Clean\t\t${GREEN}[OK]${RESET}" +.PHONY:clean + +## Test + +test: ## Run the tests of the project +ifeq ($(EXPORT_RESULT), true) + @mkdir -p $(OUTDIR) + $(eval OUTPUT_OPTIONS = | go-junit-report -set-exit-code > $(OUTDIR)/junit-report.xml) +endif + $(GOTEST) -v $(GO_OPT) ./... $(OUTPUT_OPTIONS) + @$(ECHO_CMD) "Test\t\t${GREEN}[OK]${RESET}" +.PHONY:test + + +## Lint + +lint: ## Run all available linters. + go vet ./... + errcheck ./... + staticcheck ./... + usestdlibvars ./... + shadow ./... + @$(ECHO_CMD) "Lint\t\t${GREEN}[OK]${RESET}" +.PHONY:lint + +golangci-lint: ## Run golangci-lint linter + @golangci-lint run + @$(ECHO_CMD) "GolangCI Lint\t${GREEN}[OK]${RESET}" +.PHONY:golangci-lint + +## Help + +help: ## Show this help. + @$(ECHO_CMD) '' + @$(ECHO_CMD) 'Usage:' + @$(ECHO_CMD) ' ${YELLOW}make${RESET} ${GREEN}${RESET}' + @$(ECHO_CMD) '' + @$(ECHO_CMD) 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} { \ + if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \ + else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \ + }' $(MAKEFILE_LIST) +.PHONY:help diff --git a/original_dst_linux.go b/original_dst_linux.go index 21c303c..7e5118e 100644 --- a/original_dst_linux.go +++ b/original_dst_linux.go @@ -1,12 +1,14 @@ +//go:build linux // +build linux package transocks import ( - syscall "golang.org/x/sys/unix" "net" "os" "unsafe" + + syscall "golang.org/x/sys/unix" ) func getsockopt(s int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) { @@ -31,7 +33,7 @@ func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() fd := int(f.Fd()) // revert to non-blocking mode. @@ -43,17 +45,14 @@ func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) { v6 := conn.LocalAddr().(*net.TCPAddr).IP.To4() == nil if v6 { var addr syscall.RawSockaddrInet6 - var len uint32 - len = uint32(unsafe.Sizeof(addr)) + len := uint32(unsafe.Sizeof(addr)) err = getsockopt(fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, unsafe.Pointer(&addr), &len) if err != nil { return nil, os.NewSyscallError("getsockopt", err) } ip := make([]byte, 16) - for i, b := range addr.Addr { - ip[i] = b - } + copy(ip, addr.Addr[:]) pb := *(*[2]byte)(unsafe.Pointer(&addr.Port)) return &net.TCPAddr{ IP: ip, @@ -63,17 +62,14 @@ func GetOriginalDST(conn *net.TCPConn) (*net.TCPAddr, error) { // IPv4 var addr syscall.RawSockaddrInet4 - var len uint32 - len = uint32(unsafe.Sizeof(addr)) + len := uint32(unsafe.Sizeof(addr)) err = getsockopt(fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, unsafe.Pointer(&addr), &len) if err != nil { return nil, os.NewSyscallError("getsockopt", err) } ip := make([]byte, 4) - for i, b := range addr.Addr { - ip[i] = b - } + copy(ip, addr.Addr[:]) pb := *(*[2]byte)(unsafe.Pointer(&addr.Port)) return &net.TCPAddr{ IP: ip, diff --git a/original_dst_linux_test.go b/original_dst_linux_test.go index 0210d89..1799434 100644 --- a/original_dst_linux_test.go +++ b/original_dst_linux_test.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package transocks @@ -14,13 +15,14 @@ func TestGetOriginalDST(t *testing.T) { if err != nil { t.Fatal(err) } - defer l.Close() + + defer func() { _ = l.Close() }() c, err := l.Accept() if err != nil { t.Fatal(err) } - defer c.Close() + defer func() { _ = c.Close() }() origAddr, err := GetOriginalDST(c.(*net.TCPConn)) if err != nil { diff --git a/scripts/changes.awk b/scripts/changes.awk new file mode 100644 index 0000000..4c8e834 --- /dev/null +++ b/scripts/changes.awk @@ -0,0 +1,20 @@ +# Get changes of given version number. +{ + while (index($0, "## [" version "]") <= 0) { + if (getline <= 0) { + exit + } + } + if (getline <= 0 ) { + exit + } + if (getline <= 0 ) { + exit + } + while (index($0, "## [") <= 0) { + print $0 + if (getline <= 0) { + exit + } + } +} \ No newline at end of file diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 0000000..7f922b2 --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,22 @@ +#!/bin/bash +if [ -z ${TAG_NAME+x} ]; then + if [ -z ${BRANCH_NAME+x} ]; then + BRANCH_NAME=$(echo $(git branch --show-current) || \ + echo $(git name-rev --name-only HEAD)) + fi + GIT_VERSION=$(echo ${BRANCH_NAME} | grep -q 'release/' \ + && echo ${BRANCH_NAME} | sed -e 's|release/|v|' -e 's/$/-RC/' || \ + echo $(git describe --always --tags --dirty 2>/dev/null) || echo v0) +else + GIT_VERSION=${TAG_NAME} +fi + +if [ -z ${VERSION+x} ]; then + VERSION=$(echo ${GIT_VERSION} | sed -e 's|^origin/||') +fi + +if [ -z $1 ]; then + echo "${VERSION}" +else + echo ${VERSION} | sed -e 's/^v//' +fi diff --git a/server.go b/server.go index a6c00c9..6bb4d97 100644 --- a/server.go +++ b/server.go @@ -85,7 +85,7 @@ func NewServer(c *Config) (*Server, error) { func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { tc, ok := conn.(*net.TCPConn) if !ok { - s.logger.Error("non-TCP connection", map[string]interface{}{ + _ = s.logger.Error("non-TCP connection", map[string]interface{}{ "conn": conn, }) return @@ -101,7 +101,7 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { origAddr, err := GetOriginalDST(tc) if err != nil { fields[log.FnError] = err.Error() - s.logger.Error("GetOriginalDST failed", fields) + _ = s.logger.Error("GetOriginalDST failed", fields) return } addr = origAddr.String() @@ -109,14 +109,20 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { addr = tc.LocalAddr().String() } - var reader io.Reader = tc + var ( + reader io.Reader = tc + isTLS bool + reader_n io.Reader + ) // Check if TLS - isTLS, reader_n, err := peekSSL(tc) - if err != nil { + if isTLSloc, reader_nloc, err := peekSSL(tc); err != nil { fields[log.FnError] = err.Error() - s.logger.Error("peekSSL failed", fields) + _ = s.logger.Error("peekSSL failed", fields) return + } else { + isTLS = isTLSloc + reader_n = reader_nloc } reader = reader_n fields["is_tls"] = isTLS @@ -126,7 +132,7 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { hello, reader_n2, err := peekClientHello(reader) if err != nil { fields[log.FnError] = err.Error() - s.logger.Warn("peekClientHello failed", fields) + _ = s.logger.Warn("peekClientHello failed", fields) } if err == nil && hello.ServerName != "" { addr = hello.ServerName + addr[strings.Index(addr, ":"):] @@ -137,7 +143,7 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { host, reader_n3, err := peekHTTP(reader) if err != nil { fields[log.FnError] = err.Error() - s.logger.Warn("peekHTTP failed", fields) + _ = s.logger.Warn("peekHTTP failed", fields) } else { if err == nil && host != "" { addr = host + addr[strings.Index(addr, ":"):] @@ -151,33 +157,33 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { destConn, err := s.dialer.Dial("tcp", addr) if err != nil { fields[log.FnError] = err.Error() - s.logger.Error("failed to connect to proxy server", fields) + _ = s.logger.Error("failed to connect to proxy server", fields) return } - defer destConn.Close() + defer func() { _ = destConn.Close() }() - s.logger.Info("proxy starts", fields) + _ = s.logger.Info("proxy starts", fields) // do proxy st := time.Now() env := well.NewEnvironment(ctx) env.Go(func(ctx context.Context) error { buf := s.pool.Get().([]byte) - _, err := io.CopyBuffer(destConn, reader, buf) - s.pool.Put(buf) + _, err = io.CopyBuffer(destConn, reader, buf) + s.pool.Put(&buf) if hc, ok := destConn.(netutil.HalfCloser); ok { - hc.CloseWrite() + _ = hc.CloseWrite() } - tc.CloseRead() + _ = tc.CloseRead() return err }) env.Go(func(ctx context.Context) error { buf := s.pool.Get().([]byte) - _, err := io.CopyBuffer(tc, destConn, buf) - s.pool.Put(buf) - tc.CloseWrite() + _, err = io.CopyBuffer(tc, destConn, buf) + s.pool.Put(&buf) + _ = tc.CloseWrite() if hc, ok := destConn.(netutil.HalfCloser); ok { - hc.CloseRead() + _ = hc.CloseRead() } return err }) @@ -188,10 +194,10 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) { fields["elapsed"] = time.Since(st).Seconds() if err != nil { fields[log.FnError] = err.Error() - s.logger.Error("proxy ends with an error", fields) + _ = s.logger.Error("proxy ends with an error", fields) return } - s.logger.Info("proxy ends", fields) + _ = s.logger.Info("proxy ends", fields) } // Peek ClientHello message from conn and returns SNI.