├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── main.yml ├── .env.example ├── vendor ├── github.com │ ├── caarlos0 │ │ └── env │ │ │ └── v6 │ │ │ ├── .gitignore │ │ │ ├── .goreleaser.yml │ │ │ ├── .golangci.yml │ │ │ ├── env_unix.go │ │ │ ├── env_windows.go │ │ │ ├── Makefile │ │ │ ├── LICENSE.md │ │ │ ├── README.md │ │ │ └── env.go │ └── armon │ │ └── go-socks5 │ │ ├── .travis.yml │ │ ├── .gitignore │ │ ├── credentials.go │ │ ├── resolver.go │ │ ├── LICENSE │ │ ├── ruleset.go │ │ ├── README.md │ │ ├── auth.go │ │ ├── socks5.go │ │ └── request.go ├── modules.txt └── golang.org │ └── x │ └── net │ ├── PATENTS │ ├── LICENSE │ └── context │ └── context.go ├── docker-compose.yml ├── docker-compose.build.yml ├── go.mod ├── Dockerfile ├── Dockerfile.armv6 ├── go.sum ├── ruleset.go ├── LICENSE ├── Changelog.md ├── server.go └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: serjs 2 | ko_fi: serjsj 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PROXY_USER=someuser 2 | PROXY_PASSWORD=somepass 3 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | bin 3 | card.png 4 | dist 5 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.1 4 | - tip 5 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | includes: 2 | - from_url: 3 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml 4 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - thelper 4 | - gofumpt 5 | - tparallel 6 | - unconvert 7 | - unparam 8 | - wastedassign 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | socks5: 5 | restart: always 6 | image: serjs/go-socks5-proxy 7 | env_file: .env 8 | ports: 9 | - "1080:1080" 10 | -------------------------------------------------------------------------------- /docker-compose.build.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | socks5-build: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | container_name: socks5-build 8 | env_file: .env 9 | ports: 10 | - "1080:1080" 11 | restart: unless-stopped 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/serjs/socks5-server 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 7 | github.com/caarlos0/env/v6 v6.10.1 8 | ) 9 | 10 | require golang.org/x/net v0.46.0 11 | 12 | replace github.com/armon/go-socks5 => github.com/serjs/go-socks5 v0.0.0-20250923183437-3920b97ee0d2 13 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOLANG_VERSION="1.25" 2 | 3 | FROM golang:$GOLANG_VERSION-alpine as builder 4 | RUN apk --no-cache add tzdata 5 | WORKDIR /go/src/github.com/serjs/socks5 6 | COPY . . 7 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5 8 | 9 | FROM gcr.io/distroless/static:nonroot 10 | COPY --from=builder /go/src/github.com/serjs/socks5/socks5 / 11 | ENTRYPOINT ["/socks5"] 12 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/env_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 3 | 4 | package env 5 | 6 | import "strings" 7 | 8 | func toMap(env []string) map[string]string { 9 | r := map[string]string{} 10 | for _, e := range env { 11 | p := strings.SplitN(e, "=", 2) 12 | r[p[0]] = p[1] 13 | } 14 | return r 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile.armv6: -------------------------------------------------------------------------------- 1 | ARG GOLANG_VERSION="1.19.1" 2 | 3 | FROM golang:$GOLANG_VERSION-alpine as builder 4 | RUN apk --no-cache add tzdata 5 | WORKDIR /go/src/github.com/serjs/socks5 6 | COPY . . 7 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5 8 | 9 | FROM scratch 10 | COPY --from=builder /go/src/github.com/serjs/socks5/socks5 / 11 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 12 | ENTRYPOINT ["/socks5"] 13 | -------------------------------------------------------------------------------- /vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 => github.com/serjs/go-socks5 v0.0.0-20250923183437-3920b97ee0d2 2 | ## explicit 3 | github.com/armon/go-socks5 4 | # github.com/caarlos0/env/v6 v6.10.1 5 | ## explicit; go 1.17 6 | github.com/caarlos0/env/v6 7 | # golang.org/x/net v0.46.0 8 | ## explicit; go 1.24.0 9 | golang.org/x/net/context 10 | # github.com/armon/go-socks5 => github.com/serjs/go-socks5 v0.0.0-20250923183437-3920b97ee0d2 11 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/credentials.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | // CredentialStore is used to support user/pass authentication 4 | type CredentialStore interface { 5 | Valid(user, password string) bool 6 | } 7 | 8 | // StaticCredentials enables using a map directly as a credential store 9 | type StaticCredentials map[string]string 10 | 11 | func (s StaticCredentials) Valid(user, password string) bool { 12 | pass, ok := s[user] 13 | if !ok { 14 | return false 15 | } 16 | return password == pass 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= 2 | github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= 3 | github.com/serjs/go-socks5 v0.0.0-20250923183437-3920b97ee0d2 h1:dIXY/Lrkd1rGXcN60Fc0M2x0p5/C2XcbBbtPCvVvAa4= 4 | github.com/serjs/go-socks5 v0.0.0-20250923183437-3920b97ee0d2/go.mod h1:N2PhU16m3olAb71DduLys4mYR3oQboD4uLJmSXCSuMA= 5 | golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= 6 | golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 7 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/resolver.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "net" 5 | 6 | "golang.org/x/net/context" 7 | ) 8 | 9 | // NameResolver is used to implement custom name resolution 10 | type NameResolver interface { 11 | Resolve(ctx context.Context, name string) (context.Context, net.IP, error) 12 | } 13 | 14 | // DNSResolver uses the system DNS to resolve host names 15 | type DNSResolver struct{} 16 | 17 | func (d DNSResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { 18 | addr, err := net.ResolveIPAddr("ip", name) 19 | if err != nil { 20 | return ctx, nil, err 21 | } 22 | return ctx, addr.IP, err 23 | } 24 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/env_windows.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import "strings" 4 | 5 | func toMap(env []string) map[string]string { 6 | r := map[string]string{} 7 | for _, e := range env { 8 | p := strings.SplitN(e, "=", 2) 9 | 10 | // On Windows, environment variables can start with '='. If so, Split at next character. 11 | // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 12 | prefixEqualSign := false 13 | if len(e) > 0 && e[0] == '=' { 14 | e = e[1:] 15 | prefixEqualSign = true 16 | } 17 | p = strings.SplitN(e, "=", 2) 18 | if prefixEqualSign { 19 | p[0] = "=" + p[0] 20 | } 21 | 22 | r[p[0]] = p[1] 23 | } 24 | return r 25 | } 26 | -------------------------------------------------------------------------------- /ruleset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/armon/go-socks5" 7 | "golang.org/x/net/context" 8 | ) 9 | 10 | // PermitDestAddrPattern returns a RuleSet which selectively allows addresses 11 | func PermitDestAddrPattern(pattern string) socks5.RuleSet { 12 | return &PermitDestAddrPatternRuleSet{pattern} 13 | } 14 | 15 | // PermitDestAddrPatternRuleSet is an implementation of the RuleSet which 16 | // enables filtering supported destination address 17 | type PermitDestAddrPatternRuleSet struct { 18 | AllowedFqdnPattern string 19 | } 20 | 21 | func (p *PermitDestAddrPatternRuleSet) Allow(ctx context.Context, req *socks5.Request) (context.Context, bool) { 22 | match, _ := regexp.MatchString(p.AllowedFqdnPattern, req.DestAddr.FQDN) 23 | return ctx, match 24 | } 25 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/Makefile: -------------------------------------------------------------------------------- 1 | SOURCE_FILES?=./... 2 | TEST_PATTERN?=. 3 | 4 | export GO111MODULE := on 5 | 6 | setup: 7 | go mod tidy 8 | .PHONY: setup 9 | 10 | build: 11 | go build 12 | .PHONY: build 13 | 14 | test: 15 | go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m 16 | .PHONY: test 17 | 18 | cover: test 19 | go tool cover -html=coverage.txt 20 | .PHONY: cover 21 | 22 | fmt: 23 | gofumpt -w -l . 24 | .PHONY: fmt 25 | 26 | lint: 27 | golangci-lint run ./... 28 | .PHONY: lint 29 | 30 | ci: build test 31 | .PHONY: ci 32 | 33 | card: 34 | wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" 35 | .PHONY: card 36 | 37 | .DEFAULT_GOAL := ci 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sergey Bogatyrets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Armon Dadgar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/ruleset.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | ) 6 | 7 | // RuleSet is used to provide custom rules to allow or prohibit actions 8 | type RuleSet interface { 9 | Allow(ctx context.Context, req *Request) (context.Context, bool) 10 | } 11 | 12 | // PermitAll returns a RuleSet which allows all types of connections 13 | func PermitAll() RuleSet { 14 | return &PermitCommand{true, true, true} 15 | } 16 | 17 | // PermitNone returns a RuleSet which disallows all types of connections 18 | func PermitNone() RuleSet { 19 | return &PermitCommand{false, false, false} 20 | } 21 | 22 | // PermitCommand is an implementation of the RuleSet which 23 | // enables filtering supported commands 24 | type PermitCommand struct { 25 | EnableConnect bool 26 | EnableBind bool 27 | EnableAssociate bool 28 | } 29 | 30 | func (p *PermitCommand) Allow(ctx context.Context, req *Request) (context.Context, bool) { 31 | switch req.Command { 32 | case ConnectCommand: 33 | return ctx, p.EnableConnect 34 | case BindCommand: 35 | return ctx, p.EnableBind 36 | case AssociateCommand: 37 | return ctx, p.EnableAssociate 38 | } 39 | 40 | return ctx, false 41 | } 42 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2022 Carlos Alexandro Becker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/README.md: -------------------------------------------------------------------------------- 1 | go-socks5 [![Build Status](https://travis-ci.org/armon/go-socks5.png)](https://travis-ci.org/armon/go-socks5) 2 | ========= 3 | 4 | Provides the `socks5` package that implements a [SOCKS5 server](http://en.wikipedia.org/wiki/SOCKS). 5 | SOCKS (Secure Sockets) is used to route traffic between a client and server through 6 | an intermediate proxy layer. This can be used to bypass firewalls or NATs. 7 | 8 | Feature 9 | ======= 10 | 11 | The package has the following features: 12 | * "No Auth" mode 13 | * User/Password authentication 14 | * Support for the CONNECT command 15 | * Rules to do granular filtering of commands 16 | * Custom DNS resolution 17 | * Unit tests 18 | 19 | TODO 20 | ==== 21 | 22 | The package still needs the following: 23 | * Support for the BIND command 24 | * Support for the ASSOCIATE command 25 | 26 | 27 | Example 28 | ======= 29 | 30 | Below is a simple example of usage 31 | 32 | ```go 33 | // Create a SOCKS5 server 34 | conf := &socks5.Config{} 35 | server, err := socks5.New(conf) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | // Create SOCKS5 proxy on localhost port 8000 41 | if err := server.ListenAndServe("tcp", "127.0.0.1:8000"); err != nil { 42 | panic(err) 43 | } 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [Unreleased - available on :latest tag for docker image] 5 | ### Changed 6 | ### Added 7 | 8 | ## [v0.0.4] - 2025-10-07 9 | 10 | **Important:** :warning: Starting with this release, the proxy requires authentication by default. The `REQUIRE_AUTH` parameter is now set to `true` by default. Please refer to the documentation for details on this change. 11 | 12 | ### Changed 13 | - Migrated to a distroless Docker image from scratch. 14 | - Moved go-socks5 to a modified forked repository at https://github.com/serjs/go-socks5 as a dependency. 15 | 16 | ### Added 17 | - Added `REQUIRE_AUTH` parameter with a default value of `true` to enforce authentication for the proxy. 18 | - Added `ALLOWED_DEST_FQDN` config environment parameter for filtering destination FQDNs based on regex patterns. 19 | - Added `SetIPWhitelist` config environment parameter for setting a whitelist of IP addresses allowed to use the proxy connection. 20 | - Implemented Dependabot version updates automation. 21 | 22 | ## [v0.0.3] - 2021-07-07 23 | ### Added 24 | - TZ env varible support for scratch image 25 | 26 | ### Changed 27 | - Update golang to 1.16.5 28 | - Migrate to go module 29 | 30 | ## [v0.0.2] - 2020-03-21 31 | ### Added 32 | - PROXY_PORT env parameter for app 33 | - Multiarch support for docker images 34 | 35 | ### Changed 36 | ADd caarlos0/env lib for working with ENV variables 37 | 38 | ## [v0.0.1] - 2018-04-24 39 | ### Added 40 | - Optional auth 41 | 42 | ### Changed 43 | - Golang vendoring 44 | - Change Dockerfile for multistage builds with final scratch image 45 | 46 | ### Removed 47 | - IDE files 48 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Latest tag from master branch" 2 | env: 3 | DOCKERHUB_REPOSITORY: serjs/go-socks5-proxy 4 | 5 | on: 6 | pull_request: 7 | branches: master 8 | push: 9 | branches: master 10 | release: 11 | types: [published, edited] 12 | 13 | jobs: 14 | hadolint: 15 | name: hadolint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@master 19 | - name: hadolint 20 | uses: hadolint/hadolint-action@v1.5.0 21 | with: 22 | ignore: DL3018 23 | docker: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - 27 | name: Checkout 28 | uses: actions/checkout@v1 29 | - 30 | name: Set up Docker Buildx 31 | id: buildx 32 | uses: crazy-max/ghaction-docker-buildx@v1.1.0 33 | - 34 | name: Print builder available platforms 35 | run: echo ${{ steps.buildx.outputs.platforms }} 36 | - 37 | id: meta 38 | uses: docker/metadata-action@v3 39 | with: 40 | images: serjs/go-socks5-proxy 41 | tags: | 42 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 43 | type=ref,event=branch 44 | type=ref,event=pr 45 | type=semver,pattern={{raw}} 46 | - 47 | name: Dockerhub login 48 | uses: docker/login-action@v2 49 | if: github.event_name != 'pull_request' 50 | with: 51 | username: ${{ secrets.DOCKERHUB_USERNAME }} 52 | password: ${{ secrets.DOCKERHUB_TOKEN }} 53 | - 54 | name: Run Buildx for amd64, armv7, arm64 architectures 55 | uses: docker/build-push-action@v2 56 | with: 57 | context: . 58 | platforms: linux/amd64,linux/arm/v7,linux/arm64 59 | push: ${{ github.event_name != 'pull_request' }} 60 | tags: ${{ steps.meta.outputs.tags }} 61 | labels: ${{ steps.meta.outputs.labels }} 62 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "github.com/armon/go-socks5" 8 | "github.com/caarlos0/env/v6" 9 | ) 10 | 11 | type params struct { 12 | User string `env:"PROXY_USER" envDefault:""` 13 | Password string `env:"PROXY_PASSWORD" envDefault:""` 14 | Port string `env:"PROXY_PORT" envDefault:"1080"` 15 | AllowedDestFqdn string `env:"ALLOWED_DEST_FQDN" envDefault:""` 16 | AllowedIPs []string `env:"ALLOWED_IPS" envSeparator:"," envDefault:""` 17 | ListenIP string `env:"PROXY_LISTEN_IP" envDefault:"0.0.0.0"` 18 | RequireAuth bool `env:"REQUIRE_AUTH" envDefault:"true"` 19 | } 20 | 21 | func main() { 22 | // Working with app params 23 | cfg := params{} 24 | err := env.Parse(&cfg) 25 | if err != nil { 26 | log.Printf("%+v\n", err) 27 | } 28 | 29 | //Initialize socks5 config 30 | socks5conf := &socks5.Config{ 31 | Logger: log.New(os.Stdout, "", log.LstdFlags), 32 | } 33 | 34 | if cfg.RequireAuth { 35 | if cfg.User == "" || cfg.Password == "" { 36 | log.Fatalln("Error: REQUIRE_AUTH is true, but PROXY_USER and PROXY_PASSWORD are not set. The application will now exit.") 37 | } 38 | creds := socks5.StaticCredentials{ 39 | cfg.User: cfg.Password, 40 | } 41 | cator := socks5.UserPassAuthenticator{Credentials: creds} 42 | socks5conf.AuthMethods = []socks5.Authenticator{cator} 43 | } else { 44 | log.Println("Warning: Running the proxy server without authentication. This is NOT recommended for public servers.") 45 | } 46 | 47 | if cfg.AllowedDestFqdn != "" { 48 | socks5conf.Rules = PermitDestAddrPattern(cfg.AllowedDestFqdn) 49 | } 50 | 51 | server, err := socks5.New(socks5conf) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | // Set IP whitelist 57 | if len(cfg.AllowedIPs) > 0 { 58 | whitelist := make([]net.IP, len(cfg.AllowedIPs)) 59 | for i, ip := range cfg.AllowedIPs { 60 | whitelist[i] = net.ParseIP(ip) 61 | } 62 | server.SetIPWhitelist(whitelist) 63 | } 64 | 65 | listenAddr := ":" + cfg.Port 66 | if cfg.ListenIP != "" { 67 | listenAddr = cfg.ListenIP + ":" + cfg.Port 68 | } 69 | 70 | 71 | log.Printf("Start listening proxy service on %s\n", listenAddr) 72 | if err := server.ListenAndServe("tcp", listenAddr); err != nil { 73 | log.Fatal(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-socks5-proxy 2 | 3 | ![Latest tag from master branch](https://github.com/serjs/socks5-server/workflows/Latest%20tag%20from%20master%20branch/badge.svg) 4 | 5 | Simple socks5 server using go-socks5 with authentication, allowed ips list and destination FQDNs filtering 6 | 7 | # Examples 8 | 9 | - Run docker container using default container port 1080 and expose it to world using host port 1080, with auth creds 10 | 11 | ```docker run -d --name socks5 -p 1080:1080 -e PROXY_USER= -e PROXY_PASSWORD= serjs/go-socks5-proxy``` 12 | 13 | - Run docker container using specific container port and expose it to host port 1090 14 | 15 | ```docker run -d --name socks5 -p 1090:9090 -e PROXY_USER= -e PROXY_PASSWORD= -e PROXY_PORT=9090 serjs/go-socks5-proxy``` 16 | 17 | # List of supported config parameters 18 | 19 | |ENV variable|Type|Default|Description| 20 | |------------|----|-------|-----------| 21 | |REQUIRE_AUTH|String|true|Allow accepting socks5 connections without auth creds. Not recommended untill you use other protections mechanisms like Whitelists Subnets using Firewall or Proxy itself| 22 | |PROXY_USER|String|EMPTY|Set proxy user (also required existed PROXY_PASS)| 23 | |PROXY_PASSWORD|String|EMPTY|Set proxy password for auth, used with PROXY_USER| 24 | |PROXY_PORT|String|1080|Set listen port for application inside docker container| 25 | |ALLOWED_DEST_FQDN|String|EMPTY|Allowed destination address regular expression pattern. Default allows all.| 26 | |ALLOWED_IPS|String|Empty|Set allowed IP's that can connect to proxy, separator `,`| 27 | 28 | 29 | # Build your own image: 30 | `docker-compose -f docker-compose.build.yml up -d`\ 31 | Just don't forget to set parameters in the `.env` file (`cp .env.example .env)` and edit it with your config parameters 32 | 33 | # Test running service 34 | 35 | Assuming that you are using container on 1080 host docker port 36 | 37 | ## Without authentication 38 | 39 | ```curl --socks5 :1080 https://ipinfo.io``` - result must show docker host ip (for bridged network) 40 | 41 | or 42 | 43 | ```docker run --rm curlimages/curl:7.65.3 -s --socks5 :1080 https://ipinfo.io``` 44 | 45 | ## With authentication 46 | 47 | ```curl --socks5 :1080 -U : https://ipinfo.io``` 48 | 49 | or 50 | 51 | ```docker run --rm curlimages/curl:7.65.3 -s --socks5 :@:1080 https://ipinfo.io``` 52 | 53 | # Authors 54 | 55 | * **Sergey Bogayrets** 56 | 57 | See also the list of [contributors](https://github.com/serjs/socks5-server/graphs/contributors) who participated in this project. 58 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/auth.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | const ( 9 | NoAuth = uint8(0) 10 | noAcceptable = uint8(255) 11 | UserPassAuth = uint8(2) 12 | userAuthVersion = uint8(1) 13 | authSuccess = uint8(0) 14 | authFailure = uint8(1) 15 | ) 16 | 17 | var ( 18 | UserAuthFailed = fmt.Errorf("User authentication failed") 19 | NoSupportedAuth = fmt.Errorf("No supported authentication mechanism") 20 | ) 21 | 22 | // A Request encapsulates authentication state provided 23 | // during negotiation 24 | type AuthContext struct { 25 | // Provided auth method 26 | Method uint8 27 | // Payload provided during negotiation. 28 | // Keys depend on the used auth method. 29 | // For UserPassauth contains Username 30 | Payload map[string]string 31 | } 32 | 33 | type Authenticator interface { 34 | Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) 35 | GetCode() uint8 36 | } 37 | 38 | // NoAuthAuthenticator is used to handle the "No Authentication" mode 39 | type NoAuthAuthenticator struct{} 40 | 41 | func (a NoAuthAuthenticator) GetCode() uint8 { 42 | return NoAuth 43 | } 44 | 45 | func (a NoAuthAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) { 46 | _, err := writer.Write([]byte{socks5Version, NoAuth}) 47 | return &AuthContext{NoAuth, nil}, err 48 | } 49 | 50 | // UserPassAuthenticator is used to handle username/password based 51 | // authentication 52 | type UserPassAuthenticator struct { 53 | Credentials CredentialStore 54 | } 55 | 56 | func (a UserPassAuthenticator) GetCode() uint8 { 57 | return UserPassAuth 58 | } 59 | 60 | func (a UserPassAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) { 61 | // Tell the client to use user/pass auth 62 | if _, err := writer.Write([]byte{socks5Version, UserPassAuth}); err != nil { 63 | return nil, err 64 | } 65 | 66 | // Get the version and username length 67 | header := []byte{0, 0} 68 | if _, err := io.ReadAtLeast(reader, header, 2); err != nil { 69 | return nil, err 70 | } 71 | 72 | // Ensure we are compatible 73 | if header[0] != userAuthVersion { 74 | return nil, fmt.Errorf("Unsupported auth version: %v", header[0]) 75 | } 76 | 77 | // Get the user name 78 | userLen := int(header[1]) 79 | user := make([]byte, userLen) 80 | if _, err := io.ReadAtLeast(reader, user, userLen); err != nil { 81 | return nil, err 82 | } 83 | 84 | // Get the password length 85 | if _, err := reader.Read(header[:1]); err != nil { 86 | return nil, err 87 | } 88 | 89 | // Get the password 90 | passLen := int(header[0]) 91 | pass := make([]byte, passLen) 92 | if _, err := io.ReadAtLeast(reader, pass, passLen); err != nil { 93 | return nil, err 94 | } 95 | 96 | // Verify the password 97 | if a.Credentials.Valid(string(user), string(pass)) { 98 | if _, err := writer.Write([]byte{userAuthVersion, authSuccess}); err != nil { 99 | return nil, err 100 | } 101 | } else { 102 | if _, err := writer.Write([]byte{userAuthVersion, authFailure}); err != nil { 103 | return nil, err 104 | } 105 | return nil, UserAuthFailed 106 | } 107 | 108 | // Done 109 | return &AuthContext{UserPassAuth, map[string]string{"Username": string(user)}}, nil 110 | } 111 | 112 | // authenticate is used to handle connection authentication 113 | func (s *Server) authenticate(conn io.Writer, bufConn io.Reader) (*AuthContext, error) { 114 | // Get the methods 115 | methods, err := readMethods(bufConn) 116 | if err != nil { 117 | return nil, fmt.Errorf("Failed to get auth methods: %v", err) 118 | } 119 | 120 | // Select a usable method 121 | for _, method := range methods { 122 | cator, found := s.authMethods[method] 123 | if found { 124 | return cator.Authenticate(bufConn, conn) 125 | } 126 | } 127 | 128 | // No usable method found 129 | return nil, noAcceptableAuth(conn) 130 | } 131 | 132 | // noAcceptableAuth is used to handle when we have no eligible 133 | // authentication mechanism 134 | func noAcceptableAuth(conn io.Writer) error { 135 | conn.Write([]byte{socks5Version, noAcceptable}) 136 | return NoSupportedAuth 137 | } 138 | 139 | // readMethods is used to read the number of methods 140 | // and proceeding auth methods 141 | func readMethods(r io.Reader) ([]byte, error) { 142 | header := []byte{0} 143 | if _, err := r.Read(header); err != nil { 144 | return nil, err 145 | } 146 | 147 | numMethods := int(header[0]) 148 | methods := make([]byte, numMethods) 149 | _, err := io.ReadAtLeast(r, methods, numMethods) 150 | return methods, err 151 | } 152 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/socks5.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | const ( 14 | socks5Version = uint8(5) 15 | ) 16 | 17 | // Config is used to setup and configure a Server 18 | type Config struct { 19 | // AuthMethods can be provided to implement custom authentication 20 | // By default, "auth-less" mode is enabled. 21 | // For password-based auth use UserPassAuthenticator. 22 | AuthMethods []Authenticator 23 | 24 | // If provided, username/password authentication is enabled, 25 | // by appending a UserPassAuthenticator to AuthMethods. If not provided, 26 | // and AUthMethods is nil, then "auth-less" mode is enabled. 27 | Credentials CredentialStore 28 | 29 | // Resolver can be provided to do custom name resolution. 30 | // Defaults to DNSResolver if not provided. 31 | Resolver NameResolver 32 | 33 | // Rules is provided to enable custom logic around permitting 34 | // various commands. If not provided, PermitAll is used. 35 | Rules RuleSet 36 | 37 | // Rewriter can be used to transparently rewrite addresses. 38 | // This is invoked before the RuleSet is invoked. 39 | // Defaults to NoRewrite. 40 | Rewriter AddressRewriter 41 | 42 | // BindIP is used for bind or udp associate 43 | BindIP net.IP 44 | 45 | // Logger can be used to provide a custom log target. 46 | // Defaults to stdout. 47 | Logger *log.Logger 48 | 49 | // Optional function for dialing out 50 | Dial func(ctx context.Context, network, addr string) (net.Conn, error) 51 | } 52 | 53 | // Server is reponsible for accepting connections and handling 54 | // the details of the SOCKS5 protocol 55 | type Server struct { 56 | config *Config 57 | authMethods map[uint8]Authenticator 58 | isIPAllowed func(net.IP) bool 59 | } 60 | 61 | // New creates a new Server and potentially returns an error 62 | func New(conf *Config) (*Server, error) { 63 | // Ensure we have at least one authentication method enabled 64 | if len(conf.AuthMethods) == 0 { 65 | if conf.Credentials != nil { 66 | conf.AuthMethods = []Authenticator{&UserPassAuthenticator{conf.Credentials}} 67 | } else { 68 | conf.AuthMethods = []Authenticator{&NoAuthAuthenticator{}} 69 | } 70 | } 71 | 72 | // Ensure we have a DNS resolver 73 | if conf.Resolver == nil { 74 | conf.Resolver = DNSResolver{} 75 | } 76 | 77 | // Ensure we have a rule set 78 | if conf.Rules == nil { 79 | conf.Rules = PermitAll() 80 | } 81 | 82 | // Ensure we have a log target 83 | if conf.Logger == nil { 84 | conf.Logger = log.New(os.Stdout, "", log.LstdFlags) 85 | } 86 | 87 | server := &Server{ 88 | config: conf, 89 | } 90 | 91 | server.authMethods = make(map[uint8]Authenticator) 92 | 93 | for _, a := range conf.AuthMethods { 94 | server.authMethods[a.GetCode()] = a 95 | } 96 | 97 | // Set default IP whitelist function 98 | server.isIPAllowed = func(ip net.IP) bool { 99 | return true // default allow all IPs 100 | } 101 | 102 | return server, nil 103 | } 104 | 105 | // ListenAndServe is used to create a listener and serve on it 106 | func (s *Server) ListenAndServe(network, addr string) error { 107 | l, err := net.Listen(network, addr) 108 | if err != nil { 109 | return err 110 | } 111 | return s.Serve(l) 112 | } 113 | 114 | // Serve is used to serve connections from a listener 115 | func (s *Server) Serve(l net.Listener) error { 116 | for { 117 | conn, err := l.Accept() 118 | if err != nil { 119 | return err 120 | } 121 | go s.ServeConn(conn) 122 | } 123 | return nil 124 | } 125 | 126 | // SetIPWhitelist sets the function to check if a given IP is allowed 127 | func (s *Server) SetIPWhitelist(allowedIPs []net.IP) { 128 | s.isIPAllowed = func(ip net.IP) bool { 129 | for _, allowedIP := range allowedIPs { 130 | if ip.Equal(allowedIP) { 131 | return true 132 | } 133 | } 134 | return false 135 | } 136 | } 137 | 138 | // ServeConn is used to serve a single connection. 139 | func (s *Server) ServeConn(conn net.Conn) error { 140 | defer conn.Close() 141 | bufConn := bufio.NewReader(conn) 142 | 143 | // Check client IP against whitelist 144 | clientIP, _, err := net.SplitHostPort(conn.RemoteAddr().String()) 145 | if err != nil { 146 | s.config.Logger.Printf("[ERR] socks: Failed to get client IP address: %v", err) 147 | return err 148 | } 149 | ip := net.ParseIP(clientIP) 150 | if s.isIPAllowed(ip) { 151 | s.config.Logger.Printf("[INFO] socks: Connection from allowed IP address: %s", clientIP) 152 | } else { 153 | s.config.Logger.Printf("[WARN] socks: Connection from not allowed IP address: %s", clientIP) 154 | return fmt.Errorf("connection from not allowed IP address") 155 | } 156 | 157 | // Read the version byte 158 | version := []byte{0} 159 | if _, err := bufConn.Read(version); err != nil { 160 | s.config.Logger.Printf("[ERR] socks: Failed to get version byte: %v", err) 161 | return err 162 | } 163 | 164 | // Ensure we are compatible 165 | if version[0] != socks5Version { 166 | err := fmt.Errorf("Unsupported SOCKS version: %v", version) 167 | s.config.Logger.Printf("[ERR] socks: %v", err) 168 | return err 169 | } 170 | 171 | // Authenticate the connection 172 | authContext, err := s.authenticate(conn, bufConn) 173 | if err != nil { 174 | err = fmt.Errorf("Failed to authenticate: %v", err) 175 | s.config.Logger.Printf("[ERR] socks: %v", err) 176 | return err 177 | } 178 | 179 | request, err := NewRequest(bufConn) 180 | if err != nil { 181 | if err == unrecognizedAddrType { 182 | if err := sendReply(conn, addrTypeNotSupported, nil); err != nil { 183 | return fmt.Errorf("Failed to send reply: %v", err) 184 | } 185 | } 186 | return fmt.Errorf("Failed to read destination address: %v", err) 187 | } 188 | request.AuthContext = authContext 189 | if client, ok := conn.RemoteAddr().(*net.TCPAddr); ok { 190 | request.RemoteAddr = &AddrSpec{IP: client.IP, Port: client.Port} 191 | } 192 | 193 | // Process the client request 194 | if err := s.handleRequest(request, conn); err != nil { 195 | err = fmt.Errorf("Failed to handle request: %v", err) 196 | s.config.Logger.Printf("[ERR] socks: %v", err) 197 | return err 198 | } 199 | 200 | return nil 201 | } 202 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package context defines the Context type, which carries deadlines, 6 | // cancellation signals, and other request-scoped values across API boundaries 7 | // and between processes. 8 | // As of Go 1.7 this package is available in the standard library under the 9 | // name [context]. 10 | // 11 | // Incoming requests to a server should create a [Context], and outgoing 12 | // calls to servers should accept a Context. The chain of function 13 | // calls between them must propagate the Context, optionally replacing 14 | // it with a derived Context created using [WithCancel], [WithDeadline], 15 | // [WithTimeout], or [WithValue]. 16 | // 17 | // Programs that use Contexts should follow these rules to keep interfaces 18 | // consistent across packages and enable static analysis tools to check context 19 | // propagation: 20 | // 21 | // Do not store Contexts inside a struct type; instead, pass a Context 22 | // explicitly to each function that needs it. This is discussed further in 23 | // https://go.dev/blog/context-and-structs. The Context should be the first 24 | // parameter, typically named ctx: 25 | // 26 | // func DoSomething(ctx context.Context, arg Arg) error { 27 | // // ... use ctx ... 28 | // } 29 | // 30 | // Do not pass a nil [Context], even if a function permits it. Pass [context.TODO] 31 | // if you are unsure about which Context to use. 32 | // 33 | // Use context Values only for request-scoped data that transits processes and 34 | // APIs, not for passing optional parameters to functions. 35 | // 36 | // The same Context may be passed to functions running in different goroutines; 37 | // Contexts are safe for simultaneous use by multiple goroutines. 38 | // 39 | // See https://go.dev/blog/context for example code for a server that uses 40 | // Contexts. 41 | package context 42 | 43 | import ( 44 | "context" // standard library's context, as of Go 1.7 45 | "time" 46 | ) 47 | 48 | // A Context carries a deadline, a cancellation signal, and other values across 49 | // API boundaries. 50 | // 51 | // Context's methods may be called by multiple goroutines simultaneously. 52 | // 53 | //go:fix inline 54 | type Context = context.Context 55 | 56 | // Canceled is the error returned by [Context.Err] when the context is canceled 57 | // for some reason other than its deadline passing. 58 | // 59 | //go:fix inline 60 | var Canceled = context.Canceled 61 | 62 | // DeadlineExceeded is the error returned by [Context.Err] when the context is canceled 63 | // due to its deadline passing. 64 | // 65 | //go:fix inline 66 | var DeadlineExceeded = context.DeadlineExceeded 67 | 68 | // Background returns a non-nil, empty Context. It is never canceled, has no 69 | // values, and has no deadline. It is typically used by the main function, 70 | // initialization, and tests, and as the top-level Context for incoming 71 | // requests. 72 | // 73 | //go:fix inline 74 | func Background() Context { return context.Background() } 75 | 76 | // TODO returns a non-nil, empty Context. Code should use context.TODO when 77 | // it's unclear which Context to use or it is not yet available (because the 78 | // surrounding function has not yet been extended to accept a Context 79 | // parameter). 80 | // 81 | //go:fix inline 82 | func TODO() Context { return context.TODO() } 83 | 84 | // A CancelFunc tells an operation to abandon its work. 85 | // A CancelFunc does not wait for the work to stop. 86 | // A CancelFunc may be called by multiple goroutines simultaneously. 87 | // After the first call, subsequent calls to a CancelFunc do nothing. 88 | type CancelFunc = context.CancelFunc 89 | 90 | // WithCancel returns a derived context that points to the parent context 91 | // but has a new Done channel. The returned context's Done channel is closed 92 | // when the returned cancel function is called or when the parent context's 93 | // Done channel is closed, whichever happens first. 94 | // 95 | // Canceling this context releases resources associated with it, so code should 96 | // call cancel as soon as the operations running in this [Context] complete. 97 | // 98 | //go:fix inline 99 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { 100 | return context.WithCancel(parent) 101 | } 102 | 103 | // WithDeadline returns a derived context that points to the parent context 104 | // but has the deadline adjusted to be no later than d. If the parent's 105 | // deadline is already earlier than d, WithDeadline(parent, d) is semantically 106 | // equivalent to parent. The returned [Context.Done] channel is closed when 107 | // the deadline expires, when the returned cancel function is called, 108 | // or when the parent context's Done channel is closed, whichever happens first. 109 | // 110 | // Canceling this context releases resources associated with it, so code should 111 | // call cancel as soon as the operations running in this [Context] complete. 112 | // 113 | //go:fix inline 114 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { 115 | return context.WithDeadline(parent, d) 116 | } 117 | 118 | // WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). 119 | // 120 | // Canceling this context releases resources associated with it, so code should 121 | // call cancel as soon as the operations running in this [Context] complete: 122 | // 123 | // func slowOperationWithTimeout(ctx context.Context) (Result, error) { 124 | // ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) 125 | // defer cancel() // releases resources if slowOperation completes before timeout elapses 126 | // return slowOperation(ctx) 127 | // } 128 | // 129 | //go:fix inline 130 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 131 | return context.WithTimeout(parent, timeout) 132 | } 133 | 134 | // WithValue returns a derived context that points to the parent Context. 135 | // In the derived context, the value associated with key is val. 136 | // 137 | // Use context Values only for request-scoped data that transits processes and 138 | // APIs, not for passing optional parameters to functions. 139 | // 140 | // The provided key must be comparable and should not be of type 141 | // string or any other built-in type to avoid collisions between 142 | // packages using context. Users of WithValue should define their own 143 | // types for keys. To avoid allocating when assigning to an 144 | // interface{}, context keys often have concrete type 145 | // struct{}. Alternatively, exported context key variables' static 146 | // type should be a pointer or interface. 147 | // 148 | //go:fix inline 149 | func WithValue(parent Context, key, val interface{}) Context { 150 | return context.WithValue(parent, key, val) 151 | } 152 | -------------------------------------------------------------------------------- /vendor/github.com/armon/go-socks5/request.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strconv" 8 | "strings" 9 | 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | const ( 14 | ConnectCommand = uint8(1) 15 | BindCommand = uint8(2) 16 | AssociateCommand = uint8(3) 17 | ipv4Address = uint8(1) 18 | fqdnAddress = uint8(3) 19 | ipv6Address = uint8(4) 20 | ) 21 | 22 | const ( 23 | successReply uint8 = iota 24 | serverFailure 25 | ruleFailure 26 | networkUnreachable 27 | hostUnreachable 28 | connectionRefused 29 | ttlExpired 30 | commandNotSupported 31 | addrTypeNotSupported 32 | ) 33 | 34 | var ( 35 | unrecognizedAddrType = fmt.Errorf("Unrecognized address type") 36 | ) 37 | 38 | // AddressRewriter is used to rewrite a destination transparently 39 | type AddressRewriter interface { 40 | Rewrite(ctx context.Context, request *Request) (context.Context, *AddrSpec) 41 | } 42 | 43 | // AddrSpec is used to return the target AddrSpec 44 | // which may be specified as IPv4, IPv6, or a FQDN 45 | type AddrSpec struct { 46 | FQDN string 47 | IP net.IP 48 | Port int 49 | } 50 | 51 | func (a *AddrSpec) String() string { 52 | if a.FQDN != "" { 53 | return fmt.Sprintf("%s (%s):%d", a.FQDN, a.IP, a.Port) 54 | } 55 | return fmt.Sprintf("%s:%d", a.IP, a.Port) 56 | } 57 | 58 | // Address returns a string suitable to dial; prefer returning IP-based 59 | // address, fallback to FQDN 60 | func (a AddrSpec) Address() string { 61 | if 0 != len(a.IP) { 62 | return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port)) 63 | } 64 | return net.JoinHostPort(a.FQDN, strconv.Itoa(a.Port)) 65 | } 66 | 67 | // A Request represents request received by a server 68 | type Request struct { 69 | // Protocol version 70 | Version uint8 71 | // Requested command 72 | Command uint8 73 | // AuthContext provided during negotiation 74 | AuthContext *AuthContext 75 | // AddrSpec of the the network that sent the request 76 | RemoteAddr *AddrSpec 77 | // AddrSpec of the desired destination 78 | DestAddr *AddrSpec 79 | // AddrSpec of the actual destination (might be affected by rewrite) 80 | realDestAddr *AddrSpec 81 | bufConn io.Reader 82 | } 83 | 84 | type conn interface { 85 | Write([]byte) (int, error) 86 | RemoteAddr() net.Addr 87 | } 88 | 89 | // NewRequest creates a new Request from the tcp connection 90 | func NewRequest(bufConn io.Reader) (*Request, error) { 91 | // Read the version byte 92 | header := []byte{0, 0, 0} 93 | if _, err := io.ReadAtLeast(bufConn, header, 3); err != nil { 94 | return nil, fmt.Errorf("Failed to get command version: %v", err) 95 | } 96 | 97 | // Ensure we are compatible 98 | if header[0] != socks5Version { 99 | return nil, fmt.Errorf("Unsupported command version: %v", header[0]) 100 | } 101 | 102 | // Read in the destination address 103 | dest, err := readAddrSpec(bufConn) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | request := &Request{ 109 | Version: socks5Version, 110 | Command: header[1], 111 | DestAddr: dest, 112 | bufConn: bufConn, 113 | } 114 | 115 | return request, nil 116 | } 117 | 118 | // handleRequest is used for request processing after authentication 119 | func (s *Server) handleRequest(req *Request, conn conn) error { 120 | ctx := context.Background() 121 | 122 | // Resolve the address if we have a FQDN 123 | dest := req.DestAddr 124 | if dest.FQDN != "" { 125 | ctx_, addr, err := s.config.Resolver.Resolve(ctx, dest.FQDN) 126 | if err != nil { 127 | if err := sendReply(conn, hostUnreachable, nil); err != nil { 128 | return fmt.Errorf("Failed to send reply: %v", err) 129 | } 130 | return fmt.Errorf("Failed to resolve destination '%v': %v", dest.FQDN, err) 131 | } 132 | ctx = ctx_ 133 | dest.IP = addr 134 | } 135 | 136 | // Apply any address rewrites 137 | req.realDestAddr = req.DestAddr 138 | if s.config.Rewriter != nil { 139 | ctx, req.realDestAddr = s.config.Rewriter.Rewrite(ctx, req) 140 | } 141 | 142 | // Switch on the command 143 | switch req.Command { 144 | case ConnectCommand: 145 | return s.handleConnect(ctx, conn, req) 146 | case BindCommand: 147 | return s.handleBind(ctx, conn, req) 148 | case AssociateCommand: 149 | return s.handleAssociate(ctx, conn, req) 150 | default: 151 | if err := sendReply(conn, commandNotSupported, nil); err != nil { 152 | return fmt.Errorf("Failed to send reply: %v", err) 153 | } 154 | return fmt.Errorf("Unsupported command: %v", req.Command) 155 | } 156 | } 157 | 158 | // handleConnect is used to handle a connect command 159 | func (s *Server) handleConnect(ctx context.Context, conn conn, req *Request) error { 160 | // Check if this is allowed 161 | if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { 162 | if err := sendReply(conn, ruleFailure, nil); err != nil { 163 | return fmt.Errorf("Failed to send reply: %v", err) 164 | } 165 | return fmt.Errorf("Connect to %v blocked by rules", req.DestAddr) 166 | } else { 167 | ctx = ctx_ 168 | } 169 | 170 | // Attempt to connect 171 | dial := s.config.Dial 172 | if dial == nil { 173 | dial = func(ctx context.Context, net_, addr string) (net.Conn, error) { 174 | return net.Dial(net_, addr) 175 | } 176 | } 177 | target, err := dial(ctx, "tcp", req.realDestAddr.Address()) 178 | if err != nil { 179 | msg := err.Error() 180 | resp := hostUnreachable 181 | if strings.Contains(msg, "refused") { 182 | resp = connectionRefused 183 | } else if strings.Contains(msg, "network is unreachable") { 184 | resp = networkUnreachable 185 | } 186 | if err := sendReply(conn, resp, nil); err != nil { 187 | return fmt.Errorf("Failed to send reply: %v", err) 188 | } 189 | return fmt.Errorf("Connect to %v failed: %v", req.DestAddr, err) 190 | } 191 | defer target.Close() 192 | 193 | // Send success 194 | local := target.LocalAddr().(*net.TCPAddr) 195 | bind := AddrSpec{IP: local.IP, Port: local.Port} 196 | if err := sendReply(conn, successReply, &bind); err != nil { 197 | return fmt.Errorf("Failed to send reply: %v", err) 198 | } 199 | 200 | // Start proxying 201 | errCh := make(chan error, 2) 202 | go proxy(target, req.bufConn, errCh) 203 | go proxy(conn, target, errCh) 204 | 205 | // Wait 206 | for i := 0; i < 2; i++ { 207 | e := <-errCh 208 | if e != nil { 209 | // return from this function closes target (and conn). 210 | return e 211 | } 212 | } 213 | return nil 214 | } 215 | 216 | // handleBind is used to handle a connect command 217 | func (s *Server) handleBind(ctx context.Context, conn conn, req *Request) error { 218 | // Check if this is allowed 219 | if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { 220 | if err := sendReply(conn, ruleFailure, nil); err != nil { 221 | return fmt.Errorf("Failed to send reply: %v", err) 222 | } 223 | return fmt.Errorf("Bind to %v blocked by rules", req.DestAddr) 224 | } else { 225 | ctx = ctx_ 226 | } 227 | 228 | // TODO: Support bind 229 | if err := sendReply(conn, commandNotSupported, nil); err != nil { 230 | return fmt.Errorf("Failed to send reply: %v", err) 231 | } 232 | return nil 233 | } 234 | 235 | // handleAssociate is used to handle a connect command 236 | func (s *Server) handleAssociate(ctx context.Context, conn conn, req *Request) error { 237 | // Check if this is allowed 238 | if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { 239 | if err := sendReply(conn, ruleFailure, nil); err != nil { 240 | return fmt.Errorf("Failed to send reply: %v", err) 241 | } 242 | return fmt.Errorf("Associate to %v blocked by rules", req.DestAddr) 243 | } else { 244 | ctx = ctx_ 245 | } 246 | 247 | // TODO: Support associate 248 | if err := sendReply(conn, commandNotSupported, nil); err != nil { 249 | return fmt.Errorf("Failed to send reply: %v", err) 250 | } 251 | return nil 252 | } 253 | 254 | // readAddrSpec is used to read AddrSpec. 255 | // Expects an address type byte, follwed by the address and port 256 | func readAddrSpec(r io.Reader) (*AddrSpec, error) { 257 | d := &AddrSpec{} 258 | 259 | // Get the address type 260 | addrType := []byte{0} 261 | if _, err := r.Read(addrType); err != nil { 262 | return nil, err 263 | } 264 | 265 | // Handle on a per type basis 266 | switch addrType[0] { 267 | case ipv4Address: 268 | addr := make([]byte, 4) 269 | if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil { 270 | return nil, err 271 | } 272 | d.IP = net.IP(addr) 273 | 274 | case ipv6Address: 275 | addr := make([]byte, 16) 276 | if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil { 277 | return nil, err 278 | } 279 | d.IP = net.IP(addr) 280 | 281 | case fqdnAddress: 282 | if _, err := r.Read(addrType); err != nil { 283 | return nil, err 284 | } 285 | addrLen := int(addrType[0]) 286 | fqdn := make([]byte, addrLen) 287 | if _, err := io.ReadAtLeast(r, fqdn, addrLen); err != nil { 288 | return nil, err 289 | } 290 | d.FQDN = string(fqdn) 291 | 292 | default: 293 | return nil, unrecognizedAddrType 294 | } 295 | 296 | // Read the port 297 | port := []byte{0, 0} 298 | if _, err := io.ReadAtLeast(r, port, 2); err != nil { 299 | return nil, err 300 | } 301 | d.Port = (int(port[0]) << 8) | int(port[1]) 302 | 303 | return d, nil 304 | } 305 | 306 | // sendReply is used to send a reply message 307 | func sendReply(w io.Writer, resp uint8, addr *AddrSpec) error { 308 | // Format the address 309 | var addrType uint8 310 | var addrBody []byte 311 | var addrPort uint16 312 | switch { 313 | case addr == nil: 314 | addrType = ipv4Address 315 | addrBody = []byte{0, 0, 0, 0} 316 | addrPort = 0 317 | 318 | case addr.FQDN != "": 319 | addrType = fqdnAddress 320 | addrBody = append([]byte{byte(len(addr.FQDN))}, addr.FQDN...) 321 | addrPort = uint16(addr.Port) 322 | 323 | case addr.IP.To4() != nil: 324 | addrType = ipv4Address 325 | addrBody = []byte(addr.IP.To4()) 326 | addrPort = uint16(addr.Port) 327 | 328 | case addr.IP.To16() != nil: 329 | addrType = ipv6Address 330 | addrBody = []byte(addr.IP.To16()) 331 | addrPort = uint16(addr.Port) 332 | 333 | default: 334 | return fmt.Errorf("Failed to format address: %v", addr) 335 | } 336 | 337 | // Format the message 338 | msg := make([]byte, 6+len(addrBody)) 339 | msg[0] = socks5Version 340 | msg[1] = resp 341 | msg[2] = 0 // Reserved 342 | msg[3] = addrType 343 | copy(msg[4:], addrBody) 344 | msg[4+len(addrBody)] = byte(addrPort >> 8) 345 | msg[4+len(addrBody)+1] = byte(addrPort & 0xff) 346 | 347 | // Send the message 348 | _, err := w.Write(msg) 349 | return err 350 | } 351 | 352 | type closeWriter interface { 353 | CloseWrite() error 354 | } 355 | 356 | // proxy is used to suffle data from src to destination, and sends errors 357 | // down a dedicated channel 358 | func proxy(dst io.Writer, src io.Reader, errCh chan error) { 359 | _, err := io.Copy(dst, src) 360 | if tcpConn, ok := dst.(closeWriter); ok { 361 | tcpConn.CloseWrite() 362 | } 363 | errCh <- err 364 | } 365 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/README.md: -------------------------------------------------------------------------------- 1 | # env 2 | 3 | [![Build Status](https://img.shields.io/github/workflow/status/caarlos0/env/build?style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) 4 | [![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env) 5 | [![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v6) 6 | 7 | A simple and zero-dependencies library to parse environment variables into structs. 8 | 9 | ## Example 10 | 11 | Get the module with: 12 | 13 | ```sh 14 | go get github.com/caarlos0/env/v6 15 | ``` 16 | 17 | The usage looks like this: 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "time" 25 | 26 | "github.com/caarlos0/env/v6" 27 | ) 28 | 29 | type config struct { 30 | Home string `env:"HOME"` 31 | Port int `env:"PORT" envDefault:"3000"` 32 | Password string `env:"PASSWORD,unset"` 33 | IsProduction bool `env:"PRODUCTION"` 34 | Hosts []string `env:"HOSTS" envSeparator:":"` 35 | Duration time.Duration `env:"DURATION"` 36 | TempFolder string `env:"TEMP_FOLDER" envDefault:"${HOME}/tmp" envExpand:"true"` 37 | } 38 | 39 | func main() { 40 | cfg := config{} 41 | if err := env.Parse(&cfg); err != nil { 42 | fmt.Printf("%+v\n", err) 43 | } 44 | 45 | fmt.Printf("%+v\n", cfg) 46 | } 47 | ``` 48 | 49 | You can run it like this: 50 | 51 | ```sh 52 | $ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go 53 | {Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s} 54 | ``` 55 | 56 | ⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**. 57 | 58 | ## Supported types and defaults 59 | 60 | Out of the box all built-in types are supported, plus a few others that 61 | are commonly used. 62 | 63 | Complete list: 64 | 65 | - `string` 66 | - `bool` 67 | - `int` 68 | - `int8` 69 | - `int16` 70 | - `int32` 71 | - `int64` 72 | - `uint` 73 | - `uint8` 74 | - `uint16` 75 | - `uint32` 76 | - `uint64` 77 | - `float32` 78 | - `float64` 79 | - `time.Duration` 80 | - `encoding.TextUnmarshaler` 81 | - `url.URL` 82 | 83 | Pointers, slices and slices of pointers of those types are also supported. 84 | 85 | You can also use/define a [custom parser func](#custom-parser-funcs) for any 86 | other type you want. 87 | 88 | If you set the `envDefault` tag for something, this value will be used in the 89 | case of absence of it in the environment. 90 | 91 | By default, slice types will split the environment value on `,`; you can change 92 | this behavior by setting the `envSeparator` tag. 93 | 94 | If you set the `envExpand` tag, environment variables (either in `${var}` or 95 | `$var` format) in the string will be replaced according with the actual value 96 | of the variable. 97 | 98 | ## Custom Parser Funcs 99 | 100 | If you have a type that is not supported out of the box by the lib, you are able 101 | to use (or define) and pass custom parsers (and their associated `reflect.Type`) 102 | to the `env.ParseWithFuncs()` function. 103 | 104 | In addition to accepting a struct pointer (same as `Parse()`), this function 105 | also accepts a `map[reflect.Type]env.ParserFunc`. 106 | 107 | If you add a custom parser for, say `Foo`, it will also be used to parse 108 | `*Foo` and `[]Foo` types. 109 | 110 | Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v6) 111 | for more info. 112 | 113 | ### A note about `TextUnmarshaler` and `time.Time` 114 | 115 | Env supports by default anything that implements the `TextUnmarshaler` interface. 116 | That includes things like `time.Time` for example. 117 | The upside is that depending on the format you need, you don't need to change anything. 118 | The downside is that if you do need time in another format, you'll need to create your own type. 119 | 120 | Its fairly straightforward: 121 | 122 | ```go 123 | type MyTime time.Time 124 | 125 | func (t *MyTime) UnmarshalText(text []byte) error { 126 | tt, err := time.Parse("2006-01-02", string(text)) 127 | *t = MyTime(tt) 128 | return err 129 | } 130 | 131 | type Config struct { 132 | SomeTime MyTime `env:"SOME_TIME"` 133 | } 134 | ``` 135 | 136 | And then you can parse `Config` with `env.Parse`. 137 | 138 | ## Required fields 139 | 140 | The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to ensure that some environment variable is set. 141 | In the example above, an error is returned if the `config` struct is changed to: 142 | 143 | ```go 144 | type config struct { 145 | SecretKey string `env:"SECRET_KEY,required"` 146 | } 147 | ``` 148 | 149 | ## Not Empty fields 150 | 151 | While `required` demands the environment variable to be check, it doesn't check its value. 152 | If you want to make sure the environment is set and not empty, you need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). 153 | 154 | Example: 155 | 156 | ```go 157 | type config struct { 158 | SecretKey string `env:"SECRET_KEY,notEmpty"` 159 | } 160 | ``` 161 | 162 | ## Unset environment variable after reading it 163 | 164 | The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added 165 | to ensure that some environment variable is unset after reading it. 166 | 167 | Example: 168 | 169 | ```go 170 | type config struct { 171 | SecretKey string `env:"SECRET_KEY,unset"` 172 | } 173 | ``` 174 | 175 | ## From file 176 | 177 | The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added 178 | to in order to indicate that the value of the variable shall be loaded from a file. The path of that file is given 179 | by the environment variable associated with it 180 | Example below 181 | 182 | ```go 183 | package main 184 | 185 | import ( 186 | "fmt" 187 | "time" 188 | "github.com/caarlos0/env/v6" 189 | ) 190 | 191 | type config struct { 192 | Secret string `env:"SECRET,file"` 193 | Password string `env:"PASSWORD,file" envDefault:"/tmp/password"` 194 | Certificate string `env:"CERTIFICATE,file" envDefault:"${CERTIFICATE_FILE}" envExpand:"true"` 195 | } 196 | 197 | func main() { 198 | cfg := config{} 199 | if err := env.Parse(&cfg); err != nil { 200 | fmt.Printf("%+v\n", err) 201 | } 202 | 203 | fmt.Printf("%+v\n", cfg) 204 | } 205 | ``` 206 | 207 | ```sh 208 | $ echo qwerty > /tmp/secret 209 | $ echo dvorak > /tmp/password 210 | $ echo coleman > /tmp/certificate 211 | 212 | $ SECRET=/tmp/secret \ 213 | CERTIFICATE_FILE=/tmp/certificate \ 214 | go run main.go 215 | {Secret:qwerty Password:dvorak Certificate:coleman} 216 | ``` 217 | 218 | ## Options 219 | 220 | ### Environment 221 | 222 | By setting the `Options.Environment` map you can tell `Parse` to add those `keys` and `values` 223 | as env vars before parsing is done. These envs are stored in the map and never actually set by `os.Setenv`. 224 | This option effectively makes `env` ignore the OS environment variables: only the ones provided in the option are used. 225 | 226 | This can make your testing scenarios a bit more clean and easy to handle. 227 | 228 | ```go 229 | package main 230 | 231 | import ( 232 | "fmt" 233 | "log" 234 | 235 | "github.com/caarlos0/env/v6" 236 | ) 237 | 238 | type Config struct { 239 | Password string `env:"PASSWORD"` 240 | } 241 | 242 | func main() { 243 | cfg := &Config{} 244 | opts := &env.Options{Environment: map[string]string{ 245 | "PASSWORD": "MY_PASSWORD", 246 | }} 247 | 248 | // Load env vars. 249 | if err := env.Parse(cfg, opts); err != nil { 250 | log.Fatal(err) 251 | } 252 | 253 | // Print the loaded data. 254 | fmt.Printf("%+v\n", cfg.envData) 255 | } 256 | ``` 257 | 258 | ### Changing default tag name 259 | 260 | You can change what tag name to use for setting the env vars by setting the `Options.TagName` 261 | variable. 262 | 263 | For example 264 | ```go 265 | package main 266 | 267 | import ( 268 | "fmt" 269 | "log" 270 | 271 | "github.com/caarlos0/env/v6" 272 | ) 273 | 274 | type Config struct { 275 | Password string `json:"PASSWORD"` 276 | } 277 | 278 | func main() { 279 | cfg := &Config{} 280 | opts := &env.Options{TagName: "json"} 281 | 282 | // Load env vars. 283 | if err := env.Parse(cfg, opts); err != nil { 284 | log.Fatal(err) 285 | } 286 | 287 | // Print the loaded data. 288 | fmt.Printf("%+v\n", cfg.envData) 289 | } 290 | ``` 291 | 292 | ### Prefixes 293 | 294 | You can prefix sub-structs env tags, as well as a whole `env.Parse` call. 295 | 296 | Here's an example flexing it a bit: 297 | 298 | ```go 299 | package main 300 | 301 | import ( 302 | "fmt" 303 | "log" 304 | 305 | "github.com/caarlos0/env/v6" 306 | ) 307 | 308 | type Config struct { 309 | Home string `env:"HOME"` 310 | } 311 | 312 | type ComplexConfig struct { 313 | Foo Config `envPrefix:"FOO_"` 314 | Clean Config 315 | Bar Config `envPrefix:"BAR_"` 316 | Blah string `env:"BLAH"` 317 | } 318 | 319 | func main() { 320 | cfg := ComplexConfig{} 321 | if err := Parse(&cfg, Options{ 322 | Prefix: "T_", 323 | Environment: map[string]string{ 324 | "T_FOO_HOME": "/foo", 325 | "T_BAR_HOME": "/bar", 326 | "T_BLAH": "blahhh", 327 | "T_HOME": "/clean", 328 | }, 329 | }); err != nil { 330 | log.Fatal(err) 331 | } 332 | 333 | // Load env vars. 334 | if err := env.Parse(cfg, opts); err != nil { 335 | log.Fatal(err) 336 | } 337 | 338 | // Print the loaded data. 339 | fmt.Printf("%+v\n", cfg.envData) 340 | } 341 | ``` 342 | 343 | ### On set hooks 344 | 345 | You might want to listen to value sets and, for example, log something or do some other kind of logic. 346 | You can do this by passing a `OnSet` option: 347 | 348 | ```go 349 | package main 350 | 351 | import ( 352 | "fmt" 353 | "log" 354 | 355 | "github.com/caarlos0/env/v6" 356 | ) 357 | 358 | type Config struct { 359 | Username string `env:"USERNAME" envDefault:"admin"` 360 | Password string `env:"PASSWORD"` 361 | } 362 | 363 | func main() { 364 | cfg := &Config{} 365 | opts := &env.Options{ 366 | OnSet: func(tag string, value interface{}, isDefault bool) { 367 | fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) 368 | }, 369 | } 370 | 371 | // Load env vars. 372 | if err := env.Parse(cfg, opts); err != nil { 373 | log.Fatal(err) 374 | } 375 | 376 | // Print the loaded data. 377 | fmt.Printf("%+v\n", cfg.envData) 378 | } 379 | ``` 380 | 381 | ## Making all fields to required 382 | 383 | You can make all fields that don't have a default value be required by setting the `RequiredIfNoDef: true` in the `Options`. 384 | 385 | For example 386 | 387 | ```go 388 | package main 389 | 390 | import ( 391 | "fmt" 392 | "log" 393 | 394 | "github.com/caarlos0/env/v6" 395 | ) 396 | 397 | type Config struct { 398 | Username string `env:"USERNAME" envDefault:"admin"` 399 | Password string `env:"PASSWORD"` 400 | } 401 | 402 | func main() { 403 | cfg := &Config{} 404 | opts := &env.Options{RequiredIfNoDef: true} 405 | 406 | // Load env vars. 407 | if err := env.Parse(cfg, opts); err != nil { 408 | log.Fatal(err) 409 | } 410 | 411 | // Print the loaded data. 412 | fmt.Printf("%+v\n", cfg.envData) 413 | } 414 | ``` 415 | 416 | ## Defaults from code 417 | 418 | You may define default value also in code, by initialising the config data before it's filled by `env.Parse`. 419 | Default values defined as struct tags will overwrite existing values during Parse. 420 | 421 | ```go 422 | package main 423 | 424 | import ( 425 | "fmt" 426 | "log" 427 | 428 | "github.com/caarlos0/env/v6" 429 | ) 430 | 431 | type Config struct { 432 | Username string `env:"USERNAME" envDefault:"admin"` 433 | Password string `env:"PASSWORD"` 434 | } 435 | 436 | func main() { 437 | var cfg = Config{ 438 | Username: "test", 439 | Password: "123456", 440 | } 441 | 442 | if err := env.Parse(&cfg); err != nil { 443 | fmt.Println("failed:", err) 444 | } 445 | 446 | fmt.Printf("%+v", cfg) // {Username:admin Password:123456} 447 | } 448 | ``` 449 | 450 | ## Stargazers over time 451 | 452 | [![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) 453 | -------------------------------------------------------------------------------- /vendor/github.com/caarlos0/env/v6/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "encoding" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // nolint: gochecknoglobals 16 | var ( 17 | // ErrNotAStructPtr is returned if you pass something that is not a pointer to a 18 | // Struct to Parse. 19 | ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct") 20 | 21 | defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ 22 | reflect.Bool: func(v string) (interface{}, error) { 23 | return strconv.ParseBool(v) 24 | }, 25 | reflect.String: func(v string) (interface{}, error) { 26 | return v, nil 27 | }, 28 | reflect.Int: func(v string) (interface{}, error) { 29 | i, err := strconv.ParseInt(v, 10, 32) 30 | return int(i), err 31 | }, 32 | reflect.Int16: func(v string) (interface{}, error) { 33 | i, err := strconv.ParseInt(v, 10, 16) 34 | return int16(i), err 35 | }, 36 | reflect.Int32: func(v string) (interface{}, error) { 37 | i, err := strconv.ParseInt(v, 10, 32) 38 | return int32(i), err 39 | }, 40 | reflect.Int64: func(v string) (interface{}, error) { 41 | return strconv.ParseInt(v, 10, 64) 42 | }, 43 | reflect.Int8: func(v string) (interface{}, error) { 44 | i, err := strconv.ParseInt(v, 10, 8) 45 | return int8(i), err 46 | }, 47 | reflect.Uint: func(v string) (interface{}, error) { 48 | i, err := strconv.ParseUint(v, 10, 32) 49 | return uint(i), err 50 | }, 51 | reflect.Uint16: func(v string) (interface{}, error) { 52 | i, err := strconv.ParseUint(v, 10, 16) 53 | return uint16(i), err 54 | }, 55 | reflect.Uint32: func(v string) (interface{}, error) { 56 | i, err := strconv.ParseUint(v, 10, 32) 57 | return uint32(i), err 58 | }, 59 | reflect.Uint64: func(v string) (interface{}, error) { 60 | i, err := strconv.ParseUint(v, 10, 64) 61 | return i, err 62 | }, 63 | reflect.Uint8: func(v string) (interface{}, error) { 64 | i, err := strconv.ParseUint(v, 10, 8) 65 | return uint8(i), err 66 | }, 67 | reflect.Float64: func(v string) (interface{}, error) { 68 | return strconv.ParseFloat(v, 64) 69 | }, 70 | reflect.Float32: func(v string) (interface{}, error) { 71 | f, err := strconv.ParseFloat(v, 32) 72 | return float32(f), err 73 | }, 74 | } 75 | ) 76 | 77 | func defaultTypeParsers() map[reflect.Type]ParserFunc { 78 | return map[reflect.Type]ParserFunc{ 79 | reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { 80 | u, err := url.Parse(v) 81 | if err != nil { 82 | return nil, fmt.Errorf("unable to parse URL: %v", err) 83 | } 84 | return *u, nil 85 | }, 86 | reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { 87 | s, err := time.ParseDuration(v) 88 | if err != nil { 89 | return nil, fmt.Errorf("unable to parse duration: %v", err) 90 | } 91 | return s, err 92 | }, 93 | } 94 | } 95 | 96 | // ParserFunc defines the signature of a function that can be used within `CustomParsers`. 97 | type ParserFunc func(v string) (interface{}, error) 98 | 99 | // OnSetFn is a hook that can be run when a value is set. 100 | type OnSetFn func(tag string, value interface{}, isDefault bool) 101 | 102 | // Options for the parser. 103 | type Options struct { 104 | // Environment keys and values that will be accessible for the service. 105 | Environment map[string]string 106 | 107 | // TagName specifies another tagname to use rather than the default env. 108 | TagName string 109 | 110 | // RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault' 111 | RequiredIfNoDef bool 112 | 113 | // OnSet allows to run a function when a value is set 114 | OnSet OnSetFn 115 | 116 | // Prefix define a prefix for each key 117 | Prefix string 118 | 119 | // Sets to true if we have already configured once. 120 | configured bool 121 | } 122 | 123 | // configure will do the basic configurations and defaults. 124 | func configure(opts []Options) []Options { 125 | // If we have already configured the first item 126 | // of options will have been configured set to true. 127 | if len(opts) > 0 && opts[0].configured { 128 | return opts 129 | } 130 | 131 | // Created options with defaults. 132 | opt := Options{ 133 | TagName: "env", 134 | Environment: toMap(os.Environ()), 135 | configured: true, 136 | } 137 | 138 | // Loop over all opts structs and set 139 | // to opt if value is not default/empty. 140 | for _, item := range opts { 141 | if item.Environment != nil { 142 | opt.Environment = item.Environment 143 | } 144 | if item.TagName != "" { 145 | opt.TagName = item.TagName 146 | } 147 | if item.OnSet != nil { 148 | opt.OnSet = item.OnSet 149 | } 150 | if item.Prefix != "" { 151 | opt.Prefix = item.Prefix 152 | } 153 | opt.RequiredIfNoDef = item.RequiredIfNoDef 154 | } 155 | 156 | return []Options{opt} 157 | } 158 | 159 | func getOnSetFn(opts []Options) OnSetFn { 160 | return opts[0].OnSet 161 | } 162 | 163 | // getTagName returns the tag name. 164 | func getTagName(opts []Options) string { 165 | return opts[0].TagName 166 | } 167 | 168 | // getEnvironment returns the environment map. 169 | func getEnvironment(opts []Options) map[string]string { 170 | return opts[0].Environment 171 | } 172 | 173 | // Parse parses a struct containing `env` tags and loads its values from 174 | // environment variables. 175 | func Parse(v interface{}, opts ...Options) error { 176 | return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...) 177 | } 178 | 179 | // ParseWithFuncs is the same as `Parse` except it also allows the user to pass 180 | // in custom parsers. 181 | func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error { 182 | opts = configure(opts) 183 | 184 | ptrRef := reflect.ValueOf(v) 185 | if ptrRef.Kind() != reflect.Ptr { 186 | return ErrNotAStructPtr 187 | } 188 | ref := ptrRef.Elem() 189 | if ref.Kind() != reflect.Struct { 190 | return ErrNotAStructPtr 191 | } 192 | parsers := defaultTypeParsers() 193 | for k, v := range funcMap { 194 | parsers[k] = v 195 | } 196 | 197 | return doParse(ref, parsers, opts) 198 | } 199 | 200 | func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error { 201 | refType := ref.Type() 202 | 203 | var agrErr aggregateError 204 | 205 | for i := 0; i < refType.NumField(); i++ { 206 | refField := ref.Field(i) 207 | refTypeField := refType.Field(i) 208 | 209 | if err := doParseField(refField, refTypeField, funcMap, opts); err != nil { 210 | if val, ok := err.(aggregateError); ok { 211 | agrErr.errors = append(agrErr.errors, val.errors...) 212 | } else { 213 | agrErr.errors = append(agrErr.errors, err) 214 | } 215 | } 216 | } 217 | 218 | if len(agrErr.errors) == 0 { 219 | return nil 220 | } 221 | 222 | return agrErr 223 | } 224 | 225 | func doParseField(refField reflect.Value, refTypeField reflect.StructField, funcMap map[reflect.Type]ParserFunc, opts []Options) error { 226 | if !refField.CanSet() { 227 | return nil 228 | } 229 | if reflect.Ptr == refField.Kind() && refField.Elem().Kind() == reflect.Struct { 230 | return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) 231 | } 232 | if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { 233 | return ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) 234 | } 235 | value, err := get(refTypeField, opts) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | if value != "" { 241 | return set(refField, refTypeField, value, funcMap) 242 | } 243 | 244 | if reflect.Struct == refField.Kind() { 245 | return doParse(refField, funcMap, optsWithPrefix(refTypeField, opts)) 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func get(field reflect.StructField, opts []Options) (val string, err error) { 252 | var exists bool 253 | var isDefault bool 254 | var loadFile bool 255 | var unset bool 256 | var notEmpty bool 257 | 258 | required := opts[0].RequiredIfNoDef 259 | prefix := opts[0].Prefix 260 | ownKey, tags := parseKeyForOption(field.Tag.Get(getTagName(opts))) 261 | key := prefix + ownKey 262 | for _, tag := range tags { 263 | switch tag { 264 | case "": 265 | continue 266 | case "file": 267 | loadFile = true 268 | case "required": 269 | required = true 270 | case "unset": 271 | unset = true 272 | case "notEmpty": 273 | notEmpty = true 274 | default: 275 | return "", fmt.Errorf("tag option %q not supported", tag) 276 | } 277 | } 278 | expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") 279 | defaultValue, defExists := field.Tag.Lookup("envDefault") 280 | val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts)) 281 | 282 | if expand { 283 | val = os.ExpandEnv(val) 284 | } 285 | 286 | if unset { 287 | defer os.Unsetenv(key) 288 | } 289 | 290 | if required && !exists && len(ownKey) > 0 { 291 | return "", fmt.Errorf(`required environment variable %q is not set`, key) 292 | } 293 | 294 | if notEmpty && val == "" { 295 | return "", fmt.Errorf("environment variable %q should not be empty", key) 296 | } 297 | 298 | if loadFile && val != "" { 299 | filename := val 300 | val, err = getFromFile(filename) 301 | if err != nil { 302 | return "", fmt.Errorf(`could not load content of file "%s" from variable %s: %v`, filename, key, err) 303 | } 304 | } 305 | 306 | if onSetFn := getOnSetFn(opts); onSetFn != nil { 307 | onSetFn(key, val, isDefault) 308 | } 309 | return val, err 310 | } 311 | 312 | // split the env tag's key into the expected key and desired option, if any. 313 | func parseKeyForOption(key string) (string, []string) { 314 | opts := strings.Split(key, ",") 315 | return opts[0], opts[1:] 316 | } 317 | 318 | func getFromFile(filename string) (value string, err error) { 319 | b, err := os.ReadFile(filename) 320 | return string(b), err 321 | } 322 | 323 | func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) { 324 | value, exists := envs[key] 325 | switch { 326 | case (!exists || key == "") && defExists: 327 | return defaultValue, true, true 328 | case !exists: 329 | return "", false, false 330 | } 331 | 332 | return value, true, false 333 | } 334 | 335 | func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { 336 | if tm := asTextUnmarshaler(field); tm != nil { 337 | if err := tm.UnmarshalText([]byte(value)); err != nil { 338 | return newParseError(sf, err) 339 | } 340 | return nil 341 | } 342 | 343 | typee := sf.Type 344 | fieldee := field 345 | if typee.Kind() == reflect.Ptr { 346 | typee = typee.Elem() 347 | fieldee = field.Elem() 348 | } 349 | 350 | parserFunc, ok := funcMap[typee] 351 | if ok { 352 | val, err := parserFunc(value) 353 | if err != nil { 354 | return newParseError(sf, err) 355 | } 356 | 357 | fieldee.Set(reflect.ValueOf(val)) 358 | return nil 359 | } 360 | 361 | parserFunc, ok = defaultBuiltInParsers[typee.Kind()] 362 | if ok { 363 | val, err := parserFunc(value) 364 | if err != nil { 365 | return newParseError(sf, err) 366 | } 367 | 368 | fieldee.Set(reflect.ValueOf(val).Convert(typee)) 369 | return nil 370 | } 371 | 372 | if field.Kind() == reflect.Slice { 373 | return handleSlice(field, value, sf, funcMap) 374 | } 375 | 376 | return newNoParserError(sf) 377 | } 378 | 379 | func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { 380 | separator := sf.Tag.Get("envSeparator") 381 | if separator == "" { 382 | separator = "," 383 | } 384 | parts := strings.Split(value, separator) 385 | 386 | typee := sf.Type.Elem() 387 | if typee.Kind() == reflect.Ptr { 388 | typee = typee.Elem() 389 | } 390 | 391 | if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { 392 | return parseTextUnmarshalers(field, parts, sf) 393 | } 394 | 395 | parserFunc, ok := funcMap[typee] 396 | if !ok { 397 | parserFunc, ok = defaultBuiltInParsers[typee.Kind()] 398 | if !ok { 399 | return newNoParserError(sf) 400 | } 401 | } 402 | 403 | result := reflect.MakeSlice(sf.Type, 0, len(parts)) 404 | for _, part := range parts { 405 | r, err := parserFunc(part) 406 | if err != nil { 407 | return newParseError(sf, err) 408 | } 409 | v := reflect.ValueOf(r).Convert(typee) 410 | if sf.Type.Elem().Kind() == reflect.Ptr { 411 | v = reflect.New(typee) 412 | v.Elem().Set(reflect.ValueOf(r).Convert(typee)) 413 | } 414 | result = reflect.Append(result, v) 415 | } 416 | field.Set(result) 417 | return nil 418 | } 419 | 420 | func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { 421 | if reflect.Ptr == field.Kind() { 422 | if field.IsNil() { 423 | field.Set(reflect.New(field.Type().Elem())) 424 | } 425 | } else if field.CanAddr() { 426 | field = field.Addr() 427 | } 428 | 429 | tm, ok := field.Interface().(encoding.TextUnmarshaler) 430 | if !ok { 431 | return nil 432 | } 433 | return tm 434 | } 435 | 436 | func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { 437 | s := len(data) 438 | elemType := field.Type().Elem() 439 | slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) 440 | for i, v := range data { 441 | sv := slice.Index(i) 442 | kind := sv.Kind() 443 | if kind == reflect.Ptr { 444 | sv = reflect.New(elemType.Elem()) 445 | } else { 446 | sv = sv.Addr() 447 | } 448 | tm := sv.Interface().(encoding.TextUnmarshaler) 449 | if err := tm.UnmarshalText([]byte(v)); err != nil { 450 | return newParseError(sf, err) 451 | } 452 | if kind == reflect.Ptr { 453 | slice.Index(i).Set(sv) 454 | } 455 | } 456 | 457 | field.Set(slice) 458 | 459 | return nil 460 | } 461 | 462 | func newParseError(sf reflect.StructField, err error) error { 463 | return parseError{ 464 | sf: sf, 465 | err: err, 466 | } 467 | } 468 | 469 | type parseError struct { 470 | sf reflect.StructField 471 | err error 472 | } 473 | 474 | func (e parseError) Error() string { 475 | return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err) 476 | } 477 | 478 | func newNoParserError(sf reflect.StructField) error { 479 | return fmt.Errorf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) 480 | } 481 | 482 | func optsWithPrefix(field reflect.StructField, opts []Options) []Options { 483 | subOpts := make([]Options, len(opts)) 484 | copy(subOpts, opts) 485 | if prefix := field.Tag.Get("envPrefix"); prefix != "" { 486 | subOpts[0].Prefix += prefix 487 | } 488 | return subOpts 489 | } 490 | 491 | type aggregateError struct { 492 | errors []error 493 | } 494 | 495 | func (e aggregateError) Error() string { 496 | var sb strings.Builder 497 | sb.WriteString("env:") 498 | 499 | for _, err := range e.errors { 500 | sb.WriteString(fmt.Sprintf(" %v;", err.Error())) 501 | } 502 | 503 | return strings.TrimRight(sb.String(), ";") 504 | } 505 | --------------------------------------------------------------------------------