[
  {
    "path": ".gitattributes",
    "content": "* linguist-vendored\n*.go linguist-vendored=false"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ master ]\n  schedule:\n    - cron: '29 4 * * 2'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v1\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v1\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".gitignore",
    "content": "felix\n.idea\nbuilds\nbuilds/*\ntest*\n_build*\n*.exe\nrelease\ndist\n_book\n_release\n.vscode\n_nes\ndist/\n*sqlite3"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\nos:\n  - linux\n  - osx\n\n\nmatrix:\n  fast_finish: true\n  include:\n    - go: 1.12.x\n      env: CGO_ENABLED=0 GO111MODULE=on\n\\\n\ngit:\n  depth: 1\n\n\nbefore_install:\n  - go build\nscript:\n  - ./felix sshw\n\nnotifications:\n  email:\n    recipients:\n      - neochau@gmail.com\n    on_success: never"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "LDFLAGS := \"-s -w -X main.buildTime=$(shell date -u '+%Y-%m-%dT%I:%M:%S%p') -X main.gitHash=$(shell git rev-parse HEAD)\"\nGO ?= go\nGOFMT ?= gofmt \"-s\"\nPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)\nVETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/)\nGOFILES := $(shell find . -name \"*.go\" -type f -not -path \"./vendor/*\")\n\n\nrun: install\n\t./build/felix -V\ninstall:\n\tgo install -ldflags $(LDFLAGS)\nvuejs:\n\tfelix ginbin -s dist -p felixbin\n\nbuild:vuejs\n\tgo build -race -ldflags $(LDFLAGS)  -o build/felix *.go\n\nrelease:vuejs\n\tCGO_ENABLED=1 GOOS=windows GOARCH=amd64  CXX_FOR_TARGET=i686-w64-mingw32-g++ CC_FOR_TARGET=i686-w64-mingw32-gcc go build -ldflags $(LDFLAGS) -o _release/felix-amd64-win.exe *.go\n\tCGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags $(LDFLAGS) -o _release/felix-amd64-linux *.go\n\tCGO_ENABLED=1 GOOS=linux GOARCH=arm go build -ldflags $(LDFLAGS) -o _release/felix-amd64-linux-arm *.go\n\tCGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags $(LDFLAGS) -o _release/felix-amd64-darwin *.go\n\n\n.PHONY: release\n\n"
  },
  {
    "path": "README.md",
    "content": "# Felix [中文](README_zh.md)\n"
  },
  {
    "path": "README_zh.md",
    "content": "# Felix\n[![Build Status](https://travis-ci.org/libragen/felix.svg?branch=master)](https://travis-ci.org/libragen/felix)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 5.1.x   | :white_check_mark: |\n| 5.0.x   | :x:                |\n| 4.0.x   | :white_check_mark: |\n| < 4.0   | :x:                |\n\n## Reporting a Vulnerability\n\nUse this section to tell people how to report a vulnerability.\n\nTell them where to go, how often they can expect to get an update on a\nreported vulnerability, what to expect if the vulnerability is accepted or\ndeclined, etc.\n"
  },
  {
    "path": "api/api_cfip.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/mojocn/felix/model\"\n\t\"github.com/mojocn/felix/util\"\n\t\"net/http\"\n)\n\nfunc apiCfIpInit(w http.ResponseWriter, req *http.Request) {\n\tclient, err := util.NewCfIP()\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tvar rows []model.CfIp\n\tclient.AllIps(func(ip, cidr string) {\n\t\tins := model.CfIp{\n\t\t\tIP:   ip,\n\t\t\tCidr: cidr,\n\t\t}\n\t\trows = append(rows, ins)\n\t})\n\terr = model.DB().CreateInBatches(rows, 200).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, \"ok\")\n}\n\nfunc apiCfIpList(w http.ResponseWriter, req *http.Request) {\n\tvar rows []model.CfIp\n\terr := model.DB().Limit(100).Find(&rows).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, rows)\n}\n\nfunc apiCfIpUpdate(w http.ResponseWriter, req *http.Request) {\n\tins := new(model.CfIp)\n\terr := json.NewDecoder(req.Body).Decode(ins)\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\terr = model.DB().Save(ins).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, ins)\n}\n\nfunc apiCfIpCreate(w http.ResponseWriter, req *http.Request) {\n\tins := new(model.CfIp)\n\terr := json.NewDecoder(req.Body).Decode(ins)\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tins.ID = 0\n\terr = model.DB().Save(ins).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, ins)\n}\n\nfunc apiCfIpDelete(w http.ResponseWriter, req *http.Request) {\n\tins := new(model.CfIp)\n\terr := json.NewDecoder(req.Body).Decode(ins)\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tif ins.ID == 0 {\n\t\terr = fmt.Errorf(\"id can not be 0\")\n\t}\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\terr = model.DB().Delete(ins).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, ins)\n}\n"
  },
  {
    "path": "api/api_meta.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/mojocn/felix/model\"\n\t\"net/http\"\n)\n\nfunc responseJson(w http.ResponseWriter, code int, data any) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(code)\n\terr := json.NewEncoder(w).Encode(data)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\nfunc checkErr(w http.ResponseWriter, err error) (shouldReturn bool) {\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusTeapot)\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc apiMeta(w http.ResponseWriter, req *http.Request) {\n\trow := new(model.Meta)\n\terr := model.DB().First(row).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, row)\n}\n"
  },
  {
    "path": "api/api_proxy.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/mojocn/felix/model\"\n\t\"net/http\"\n)\n\nfunc apiProxyList(w http.ResponseWriter, req *http.Request) {\n\trows := []model.Proxy{}\n\terr := model.DB().Find(&rows).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, rows)\n}\n\nfunc apiProxyUpdate(w http.ResponseWriter, req *http.Request) {\n\tins := new(model.Proxy)\n\terr := json.NewDecoder(req.Body).Decode(ins)\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\terr = model.DB().Save(ins).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, ins)\n}\n\nfunc apiProxyCreate(w http.ResponseWriter, req *http.Request) {\n\tins := new(model.Proxy)\n\terr := json.NewDecoder(req.Body).Decode(ins)\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tins.ID = 0\n\terr = model.DB().Save(ins).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, ins)\n}\n\nfunc apiProxyDelete(w http.ResponseWriter, req *http.Request) {\n\tins := new(model.Proxy)\n\terr := json.NewDecoder(req.Body).Decode(ins)\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tif ins.ID == 0 {\n\t\terr = fmt.Errorf(\"id can not be 0\")\n\t}\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\terr = model.DB().Delete(ins).Error\n\tif checkErr(w, err) {\n\t\treturn\n\t}\n\tresponseJson(w, http.StatusOK, ins)\n}\n"
  },
  {
    "path": "api/app.go",
    "content": "package api\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\ntype apiHandler struct{}\n\nfunc (apiHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}\n\nfunc AdminServer(addr string) *http.Server {\n\tlog.Println(\"http api server starting on\", addr)\n\tmux := http.NewServeMux()\n\tmux.Handle(\"/api/foo\", apiHandler{})\n\tmux.HandleFunc(\"GET /api/meta\", apiMeta)\n\n\tmux.HandleFunc(\"GET /api/proxies\", apiProxyList)\n\tmux.HandleFunc(\"PATCH /api/proxies\", apiProxyUpdate)\n\tmux.HandleFunc(\"POST /api/proxies\", apiProxyCreate)\n\tmux.HandleFunc(\"DELETE /api/proxies\", apiProxyDelete)\n\n\tmux.HandleFunc(\"GET /api/cfip-init\", apiCfIpInit)\n\tmux.HandleFunc(\"GET /api/cfips\", apiCfIpList)\n\tmux.HandleFunc(\"PATCH /api/cfips\", apiCfIpUpdate)\n\tmux.HandleFunc(\"POST /api/cfips\", apiCfIpCreate)\n\tmux.HandleFunc(\"DELETE /api/cfips\", apiCfIpDelete)\n\n\tserver := &http.Server{\n\t\tAddr:    addr,\n\t\tHandler: mux,\n\t}\n\treturn server\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/mojocn/felix\n\ngo 1.22\n\nrequire (\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/oschwald/geoip2-golang v1.11.0\n\tgolang.org/x/sys v0.27.0\n\tgorm.io/driver/sqlite v1.5.6\n\tgorm.io/gorm v1.25.12\n)\n\nrequire (\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.22 // indirect\n\tgithub.com/oschwald/maxminddb-golang v1.13.0 // indirect\n\tgolang.org/x/text v0.20.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=\ngithub.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=\ngithub.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=\ngithub.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngolang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=\ngolang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=\ngolang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=\ngorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=\ngorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=\ngorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/mojocn/felix/api\"\n\t\"github.com/mojocn/felix/model\"\n\t\"github.com/mojocn/felix/socks5ws\"\n\t\"log\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n)\n\nvar (\n\tbuildTime, gitHash string\n\tuserUUID           = \"53881505-c10c-464a-8949-e57184a576a9\"\n\turl                = \"ws://demo.libragen.cn/5sdfasdf\"\n\tprotocol           = \"socks5e\" // or vless\n)\n\nfunc main() {\n\tlog.SetFlags(log.Lmicroseconds | log.Lshortfile)\n\tslog.SetLogLoggerLevel(slog.LevelDebug)\n\n\tmodel.DB()\n\tappCfg := model.Cfg()\n\n\tapp, err := socks5ws.NewClientLocalSocks5Server(fmt.Sprintf(\"127.0.0.1:%d\", appCfg.PortSocks5), \"GeoLite2-Country.mmdb\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tslog.With(\"socks5\", app.AddrSocks5).Info(\"socks5 server listening on\")\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tsignalChan := make(chan os.Signal, 1)\n\tsignal.Notify(signalChan, syscall.SIGINT, syscall.SIGABRT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL)\n\n\thttpS := api.AdminServer(fmt.Sprintf(\"127.0.0.1:%d\", appCfg.PortHttp))\n\tgo func() {\n\t\tif err := httpS.ListenAndServe(); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tsig := <-signalChan\n\t\tfmt.Printf(\"\\nReceived signal: %s\\n\", sig)\n\t\tcancel() // Cancel the context\n\n\t\t// Shutdown the server with a timeout\n\t\tshutdownCtx, shutdownCancel := context.WithTimeout(ctx, 2*time.Second)\n\t\tdefer shutdownCancel()\n\t\tif err := httpS.Shutdown(shutdownCtx); err != nil {\n\t\t\tlog.Fatalf(\"Server Shutdown Failed:%+v\", err)\n\t\t}\n\t}()\n\n\tapp.Run(ctx)\n}\n"
  },
  {
    "path": "model/config.go",
    "content": "package model\n\nimport \"log/slog\"\n\ntype Config struct {\n\tPortSocks5 int    `json:\"port_socks5\"`\n\tPortHttp   int    `json:\"port_http\"`\n\tAuthUser   string `json:\"auth_user\"`\n\tAuthPass   string `json:\"auth_pass\"`\n}\n\nvar (\n\tcfg    *Config\n\tdefCfg = Config{\n\t\tPortSocks5: 1080,\n\t\tPortHttp:   1080 + 5,\n\t\tAuthUser:   \"admin\",\n\t\tAuthPass:   \"admin\",\n\t}\n)\n\nfunc Cfg() *Config {\n\tif cfg == nil {\n\t\trow := new(Meta)\n\t\trow.Config = defCfg\n\t\terr := db.FirstOrCreate(&row).Error\n\t\tif err != nil {\n\t\t\tslog.Error(\"get config error\", \"err\", err)\n\t\t} else {\n\t\t\tcfg = &defCfg\n\t\t}\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "model/db.go",
    "content": "package model\n\nimport (\n\t\"gorm.io/driver/sqlite\" // Sqlite driver based on CGO\n\t\"gorm.io/gorm\"\n)\n\nvar db *gorm.DB\n\nfunc initDb() {\n\tvar err error\n\tdb, err = gorm.Open(sqlite.Open(\"felix.sqlite3\"), &gorm.Config{})\n\tif err != nil {\n\t\tpanic(\"failed to connect database\")\n\t}\n\tmigrate()\n}\n\nfunc DB() *gorm.DB {\n\tif db == nil {\n\t\tinitDb()\n\t}\n\treturn db\n}\n"
  },
  {
    "path": "model/helper.go",
    "content": "package model\n\nimport (\n\t\"errors\"\n\t\"gorm.io/gorm\"\n)\n\n// PaginationQ gin handler query binding struct\ntype PaginationQ struct {\n\tOk    bool        `json:\"ok\"`\n\tSize  int         `form:\"size\" json:\"size\"`\n\tPage  int         `form:\"page\" json:\"page\"`\n\tData  interface{} `json:\"data\" comment:\"muster be a pointer of slice gorm.Model\"` // save pagination list\n\tTotal int64       `json:\"total\"`\n}\n\n// SearchAll optimized pagination method for gorm\nfunc (p *PaginationQ) SearchAll(queryTx *gorm.DB) (data *PaginationQ, err error) {\n\t//99999 magic number for get all list without pagination\n\tif p.Size == 9999 || p.Size == 99999 {\n\t\terr = queryTx.Find(p.Data).Error\n\t\tp.Ok = err == nil\n\t\treturn p, err\n\t}\n\n\tif p.Size < 1 {\n\t\tp.Size = 10\n\t}\n\tif p.Page < 1 {\n\t\tp.Page = 1\n\t}\n\toffset := p.Size * (p.Page - 1)\n\terr = queryTx.Count(&p.Total).Error\n\tif err != nil {\n\t\treturn p, err\n\t}\n\terr = queryTx.Limit(p.Size).Offset(offset).Find(p.Data).Error\n\tp.Ok = err == nil\n\treturn p, err\n}\n\nfunc crudAll(p *PaginationQ, queryTx *gorm.DB, list interface{}) (int64, error) {\n\tif p.Size < 1 {\n\t\tp.Size = 10\n\t}\n\tif p.Page < 1 {\n\t\tp.Page = 1\n\t}\n\n\tvar total int64\n\terr := queryTx.Count(&total).Error\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\toffset := p.Size * (p.Page - 1)\n\terr = queryTx.Limit(p.Size).Offset(offset).Find(list).Error\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn total, err\n}\n\nfunc crudOne(m interface{}) (err error) {\n\tif err := db.First(m).Error; errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn errors.New(\"resource is not found\")\n\t}\n\treturn nil\n}\n\nfunc crudDelete(m interface{}) (err error) {\n\t//WARNING When delete a record, you need to ensure it’s primary field has value, and GORM will use the primary key to delete the record, if primary field’s blank, GORM will delete all records for the model\n\t//primary key must be not zero value\n\tdb := db.Unscoped().Delete(m)\n\tif err = db.Error; err != nil {\n\t\treturn\n\t}\n\tif db.RowsAffected != 1 {\n\t\treturn errors.New(\"resource is not found to destroy\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "model/migrate.go",
    "content": "package model\n\nimport \"log\"\n\nfunc migrate() {\n\tif db == nil {\n\t\tlog.Print(\"db is nil\")\n\t\treturn\n\t}\n\tfor _, m := range []interface{}{&CfIp{}, &Meta{}, &Proxy{}} {\n\t\tif err := db.AutoMigrate(m); err != nil {\n\t\t\tlog.Print(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "model/t_cfip.go",
    "content": "package model\n\nimport (\n\t\"gorm.io/gorm\"\n\t\"time\"\n)\n\ntype ModelBase struct {\n\tID        uint           `gorm:\"primarykey\" json:\"id\"`\n\tCreatedAt time.Time      `json:\"created_at\"`\n\tUpdatedAt time.Time      `json:\"updated_at\"`\n\tDeletedAt gorm.DeletedAt `json:\"deleted_at,omitempty\"  gorm:\"index\"`\n}\n\ntype CfIp struct {\n\tModelBase\n\tIP    string `json:\"ip\" gorm:\"type:varchar(15)\"`\n\tCidr  string `json:\"cidr\" gorm:\"type:varchar(18)\"`\n\tPorts []int  `json:\"ports\" gorm:\"type:json;serializer:json\"`\n}\n"
  },
  {
    "path": "model/t_meta.go",
    "content": "package model\n\ntype Meta struct {\n\tModelBase\n\tConfig Config `json:\"config\" gorm:\"type:json;serializer:json\"`\n}\n"
  },
  {
    "path": "model/t_proxy.go",
    "content": "package model\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Proxy struct {\n\tModelBase\n\tName string `json:\"name\" gorm:\"varchar(255)\"`\n\n\tProtocol string `json:\"protocol\" gorm:\"varchar(16)\"` //ws,wss,http2,tls,http3\n\tHost     string `json:\"host\" gorm:\"varchar(255)\"`\n\tUri      string `json:\"uri\" gorm:\"varchar(255)\"`\n\tSni      string `json:\"sni\" gorm:\"varchar(255)\"`\n\tVersion  string `json:\"version\" gorm:\"varchar(16)\"` // one socks5\n\n\tUserID    string `json:\"user_id\"`\n\tPassword  string `json:\"password\"`\n\tTrafficKb int64  `json:\"traffic_kb\" gorm:\"default:0\"`\n\tSpeedMs   int64  `json:\"speed_ms\" gorm:\"default:0\"`\n\tStatus    string `json:\"status\" gorm:\"varchar(16);default:''\"` //active, inactive\n}\n\nfunc (p *Proxy) IsActive() bool {\n\treturn p.Status == \"active\"\n}\nfunc (p *Proxy) RelayURL() string {\n\tswitch p.Protocol {\n\tcase \"ws\":\n\t\treturn fmt.Sprintf(\"ws://%s/%s\", p.Host, strings.TrimPrefix(p.Uri, \"/\"))\n\tcase \"wss\":\n\t\treturn fmt.Sprintf(\"wss://%s/%s\", p.Host, strings.TrimPrefix(p.Uri, \"/\"))\n\tcase \"tcp+tls\":\n\t\treturn fmt.Sprintf(\"tcp-tls://%s/%s\", p.Host, strings.TrimPrefix(p.Uri, \"/\"))\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"
  },
  {
    "path": "socks5ws/app.go",
    "content": "package socks5ws\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/mojocn/felix/model\"\n\t\"github.com/mojocn/felix/util\"\n\t\"io\"\n\t\"log\"\n\t\"log/slog\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype ClientLocalSocks5Server struct {\n\tAddrSocks5 string\n\tgeo        *util.GeoIP\n\tTimeout    time.Duration\n\tproxy      *model.Proxy\n}\n\nfunc NewClientLocalSocks5Server(addr, geoIpPath string) (*ClientLocalSocks5Server, error) {\n\tgeo, err := util.NewGeoIP(geoIpPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &ClientLocalSocks5Server{\n\t\tAddrSocks5: addr,\n\t\tgeo:        geo,\n\t\tTimeout:    5 * time.Minute,\n\t}, nil\n\n}\n\nfunc (ss *ClientLocalSocks5Server) fetchActiveProxy() {\n\tvar proxies []model.Proxy\n\terr := model.DB().Find(&proxies).Error\n\tif err != nil {\n\t\tslog.Error(\"failed to get proxy setting\", \"err\", err.Error())\n\t\treturn\n\t}\n\tif len(proxies) == 0 {\n\t\tslog.Error(\"no proxy setting found\")\n\t\treturn\n\t}\n\tss.proxy = &proxies[0]\n\tfor _, proxy := range proxies {\n\t\tif proxy.IsActive() {\n\t\t\tss.proxy = &proxy\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (ss *ClientLocalSocks5Server) Run(ctx context.Context) {\n\tss.fetchActiveProxy()\n\n\tlistener, err := net.Listen(\"tcp\", ss.AddrSocks5)\n\tif err != nil {\n\t\tlistener, err = net.Listen(\"tcp4\", \"127.0.0.1:0\")\n\t}\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to listen on %s: %v\", ss.AddrSocks5, err)\n\t}\n\tss.AddrSocks5 = listener.Addr().String()\n\tslog.Info(\"socks5 server listening on\", \"addr\", ss.AddrSocks5)\n\n\tdefer listener.Close()\n\tlog.Println(\"SOCKS5 server listening on: \" + ss.AddrSocks5)\n\t//proxySettingOn(ss.AddrSocks5)\n\t//defer proxySettingOff()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlog.Println(\"socks5 server exit\")\n\t\t\treturn\n\t\tdefault:\n\t\t\tconn, err := listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to accept connection: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgo ss.handleConnection(ctx, conn)\n\t\t}\n\t}\n}\n\nfunc (ss *ClientLocalSocks5Server) socks5HandShake(conn net.Conn) error {\n\tbuf := make([]byte, 2)\n\tif _, err := io.ReadFull(conn, buf); err != nil {\n\t\treturn fmt.Errorf(\"failed to read version and nmethods: %w\", err)\n\t}\n\tif buf[0] != socks5Version {\n\t\treturn fmt.Errorf(\"socks5 only. unsupported SOCKS version: %d\", buf[0])\n\t}\n\n\t// Read the supported authentication methods\n\tnMethods := int(buf[1])\n\tnMethodsData := make([]byte, nMethods)\n\tif _, err := io.ReadFull(conn, nMethodsData); err != nil {\n\t\treturn fmt.Errorf(\"failed to read methods: %w\", err)\n\t}\n\n\t// Select no authentication (0x00)\n\tif _, err := conn.Write([]byte{socks5Version, 0x00}); err != nil {\n\t\treturn fmt.Errorf(\"failed to write method selection: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (ss *ClientLocalSocks5Server) socks5Request(conn net.Conn) (*Socks5Request, error) {\n\tbuf := make([]byte, 8<<10)\n\tn, err := conn.Read(buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read request: %w\", err)\n\t}\n\tdata := buf[:n]\n\tif len(data) < 4 {\n\t\treturn nil, fmt.Errorf(\"request too short\")\n\t}\n\treturn parseSocks5Request(data, ss.geo)\n}\n\nfunc (ss *ClientLocalSocks5Server) handleConnection(outerCtx context.Context, conn net.Conn) {\n\tdefer conn.Close() // the outer for loop is not suitable for defer, so defer close here\n\tctx, cf := context.WithTimeout(outerCtx, ss.Timeout)\n\tdefer cf()\n\n\terr := ss.socks5HandShake(conn)\n\tif err != nil {\n\t\tslog.Error(\"failed to handshake\", \"err\", err.Error())\n\t\tsocks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)\n\t\treturn\n\t}\n\treq, err := ss.socks5Request(conn)\n\tif err != nil {\n\t\tslog.Error(\"failed to parse socks5 request\", \"err\", err.Error())\n\t\tsocks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)\n\t\treturn\n\t}\n\treq.Logger().Info(\"remote target\")\n\tif req.socks5Cmd == socks5CmdConnect { //tcp\n\t\trelayTcpSvr, err := ss.dispatchRelayTcpServer(ctx, req)\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to dispatch relay tcp server\", \"err\", err.Error())\n\t\t\tsocks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)\n\t\t\treturn\n\t\t}\n\t\tsocks5Response(conn, net.IPv4zero, 0, socks5ReplyOkay)\n\t\tdefer relayTcpSvr.Close()\n\t\tss.pipeTcp(ctx, conn, relayTcpSvr)\n\t\treturn\n\t} else if req.socks5Cmd == socks5CmdUdpAssoc {\n\t\tudpH, err := NewRelayUdpDirect(conn)\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to create udp handler\", \"err\", err.Error())\n\t\t\tsocks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)\n\t\t\treturn\n\t\t}\n\n\t\tdefer udpH.Close()\n\t\tudpH.PipeUdp()\n\t\treturn\n\t} else if req.socks5Cmd == socks5CmdBind {\n\t\trelayBind(conn, req)\n\t\treturn\n\t} else {\n\t\terr = fmt.Errorf(\"unknown command: %d\", req.socks5Cmd)\n\t\tslog.Error(\"unknown command\", \"err\", err.Error())\n\t\tsocks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)\n\t}\n}\n\nfunc (ss *ClientLocalSocks5Server) shouldGoDirect(req *Socks5Request) (goDirect bool) {\n\n\tif req.CountryCode == \"CN\" || req.CountryCode == \"\" {\n\t\t//empty means geo ip failed or local address\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (ss *ClientLocalSocks5Server) dispatchRelayTcpServer(ctx context.Context, req *Socks5Request) (io.ReadWriteCloser, error) {\n\tif ss.shouldGoDirect(req) {\n\t\treq.Logger().Info(\"go direct\")\n\t\treturn NewRelayTcpDirect(req)\n\t}\n\treturn NewRelayTcpSocks5e(ctx, ss.proxy, req)\n}\n\nfunc (ss *ClientLocalSocks5Server) pipeTcp(ctx context.Context, s5 net.Conn, relayRw io.ReadWriter) {\n\twg := sync.WaitGroup{}\n\twg.Add(2)\n\tgo func() {\n\t\tspan := slog.With(\"fn\", \"ws -> s5\")\n\t\tdefer func() {\n\t\t\tspan.Debug(\"wg1 done\")\n\t\t\twg.Done()\n\t\t}()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tspan.Info(\"ctx.Done exit\")\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\t//ws.SetReadDeadline(time.Now().Add(1 * time.Second))\n\t\t\t\tbuf := make([]byte, 8<<10)\n\t\t\t\tn, err := relayRw.Read(buf)\n\t\t\t\tif err != nil {\n\t\t\t\t\tspan.Error(\"relay read\", \"err\", err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t_, err = s5.Write(buf[:n])\n\t\t\t\tif err != nil {\n\t\t\t\t\tspan.Error(\"s5 write\", \"err\", err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\tgo func() { // s5 -> ws\n\t\tspan := slog.With(\"fn\", \"s5 -> ws\")\n\t\tdefer func() {\n\t\t\tspan.Debug(\"wg2 done\")\n\t\t\twg.Done()\n\t\t}()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tspan.Debug(\"ctx.Done exit\")\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tbuf := make([]byte, 8<<10)\n\t\t\t\t//s5.SetReadDeadline(time.Now().Add(20 * time.Millisecond))\n\t\t\t\tn, err := s5.Read(buf)\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tslog.Info(\"s5 read EOF\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tet := fmt.Sprintf(\"%T\", err)\n\t\t\t\t\tspan.With(\"errType\", et).Error(\"s5 read\", \"err\", err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t//ws.SetWriteDeadline(time.Now().Add(1 * time.Second))\n\t\t\t\tn, err = relayRw.Write(buf[:n])\n\t\t\t\tif err != nil {\n\t\t\t\t\tspan.Error(\"relay write\", \"err\", err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\twg.Wait()\n\tslog.Debug(\"2 goroutines is Done\")\n}\n"
  },
  {
    "path": "socks5ws/const.go",
    "content": "package socks5ws\n\nimport (\n\t\"log/slog\"\n\t\"net\"\n)\n\nconst (\n\tsocks5Version             = 0x05\n\tsocks5ReplyOkay           = 0x00\n\tsocks5ReplyFail           = 0x01\n\tsocks5ReplyReserved       = 0x00\n\tsocks5CmdConnect          = 0x01\n\tsocks5CmdBind             = 0x02\n\tsocks5CmdUdpAssoc         = 0x03\n\tsocks5AtypeIPv4           = 0x01\n\tsocks5AtypeDomain         = 0x03\n\tsocks5AtypeIPv6           = 0x04\n\tsocks5UdpFragNotSupported = 0x00\n\tsocks5UdpFragEnd          = 0x80\n\n\tbufferSize = 64 << 10\n)\n\nfunc socks5Response(conn net.Conn, ipv4 net.IP, port int, socks5OkayOrFail byte) {\n\tif socks5OkayOrFail != socks5ReplyOkay {\n\t\tipv4 = net.IPv4zero\n\t\tport = 0\n\t}\n\tif ipv4 == nil {\n\t\tipv4 = net.IPv4zero\n\t}\n\tif port < 0 || port > 65535 {\n\t\tport = 0\n\t}\n\tresponse := []byte{socks5Version, socks5OkayOrFail, socks5ReplyReserved, socks5AtypeIPv4, ipv4[0], ipv4[1], ipv4[2], ipv4[3], byte(port >> 8), byte(port & 0xff)}\n\t_, err := conn.Write(response)\n\tif err != nil {\n\t\tslog.Error(\"socks5 request rely failed to write\", \"err\", err.Error())\n\t}\n}\n"
  },
  {
    "path": "socks5ws/relay_svr.go",
    "content": "package socks5ws\n\nimport \"io\"\n\ntype RelayTcp interface {\n\tio.Reader\n\tio.Writer\n\tio.Closer\n}\n"
  },
  {
    "path": "socks5ws/relay_tcp_direct.go",
    "content": "package socks5ws\n\nimport \"net\"\n\nvar _ RelayTcp = (*RelayTcpDirect)(nil)\n\ntype RelayTcpDirect struct {\n\tconn net.Conn\n}\n\nfunc NewRelayTcpDirect(req *Socks5Request) (*RelayTcpDirect, error) {\n\tconn, err := net.Dial(\"tcp\", req.addr())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &RelayTcpDirect{conn: conn}, nil\n}\n\nfunc (r *RelayTcpDirect) Read(p []byte) (n int, err error) {\n\treturn r.conn.Read(p)\n}\n\nfunc (r *RelayTcpDirect) Write(p []byte) (n int, err error) {\n\treturn r.conn.Write(p)\n}\n\nfunc (r *RelayTcpDirect) Close() error {\n\treturn r.conn.Close()\n}\n"
  },
  {
    "path": "socks5ws/relay_tcp_socks5e.go",
    "content": "package socks5ws\n\nimport (\n\t\"context\"\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/mojocn/felix/model\"\n\t\"log/slog\"\n\t\"time\"\n)\n\nvar _ RelayTcp = (*RelayTcpSocks5e)(nil)\n\ntype RelayTcpSocks5e struct {\n\tcfg  *model.Proxy\n\treq  *Socks5Request\n\tconn *websocket.Conn\n}\n\nfunc NewRelayTcpSocks5e(ctx context.Context, cfg *model.Proxy, req *Socks5Request) (*RelayTcpSocks5e, error) {\n\tws, err := webSocketConn(ctx, cfg, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tws.SetCloseHandler(func(code int, text string) error {\n\t\tslog.Debug(\"ws has closed\", \"code\", code, \"text\", text)\n\t\treturn nil\n\t})\n\treturn &RelayTcpSocks5e{cfg: cfg, req: req, conn: ws}, nil\n}\n\nfunc (r RelayTcpSocks5e) Read(data []byte) (n int, err error) {\n\tif r.conn != nil {\n\t\t_, p, err := r.conn.ReadMessage()\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to read ws\", \"err\", err.Error())\n\t\t}\n\t\treturn copy(data, p), err\n\t}\n\treturn 0, nil\n}\n\nfunc (r RelayTcpSocks5e) Write(data []byte) (n int, err error) {\n\tif r.conn != nil {\n\t\terr = r.conn.WriteMessage(websocket.BinaryMessage, data)\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to write ws\", \"err\", err.Error())\n\t\t}\n\t\treturn len(data), err\n\t}\n\treturn 0, nil\n}\n\nfunc (r RelayTcpSocks5e) Close() error {\n\tif r.conn != nil {\n\t\terr := r.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, \"\"), time.Now().Add(time.Millisecond*20))\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to close ws\", \"err\", err.Error())\n\t\t}\n\t\treturn r.conn.Close()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "socks5ws/relay_udp_direct.go",
    "content": "package socks5ws\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"log/slog\"\n\t\"net\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype RelayUdpDirect struct {\n\ts5       net.Conn\n\trelayUdp *net.UDPConn\n\n\t// Reassembly queue for fragmented UDP packets.\n\tmu            sync.Mutex\n\tfragments     map[string][]*udpPacket // Map of DstAddr to fragments\n\thighestFrag   map[string]byte         // Track highest FRAG value for each DstAddr\n\ttimers        map[string]*time.Timer  // Map of DstAddr to reassembly timer\n\ttimerDuration time.Duration           // Timer duration\n}\n\nfunc (ud *RelayUdpDirect) addFragment(clientAddr *net.UDPAddr, frag *udpPacket) {\n\tud.mu.Lock()\n\tdefer ud.mu.Unlock()\n\tclientDstAddr := ud.clientDstAddrAsID(clientAddr, frag.dstAddr())\n\t// Initialize fragment queue and timer if not already present.\n\tif _, exists := ud.fragments[clientDstAddr]; !exists {\n\t\tud.fragments[clientDstAddr] = []*udpPacket{}\n\t\tud.highestFrag[clientDstAddr] = socks5UdpFragNotSupported\n\t\tud.startTimer(clientDstAddr)\n\t}\n\n\t// Update highest FRAG value.\n\tif frag.Frag > ud.highestFrag[clientDstAddr] {\n\t\tud.highestFrag[clientDstAddr] = frag.Frag\n\t}\n\n\t// Add fragment to the queue.\n\tud.fragments[clientDstAddr] = append(ud.fragments[clientDstAddr], frag)\n\n\t// Check if this is the final fragment (end-of-fragment sequence).\n\tif frag.Frag == socks5UdpFragEnd || frag.Frag == socks5UdpFragNotSupported { // High-order bit indicates end of sequence.\n\t\tud.assembleThenPipeUdp(clientAddr, frag.dstAddr())\n\t}\n}\n\nfunc (ud *RelayUdpDirect) startTimer(ClientDstAddr string) {\n\tif timer, exists := ud.timers[ClientDstAddr]; exists {\n\t\ttimer.Stop()\n\t}\n\tud.timers[ClientDstAddr] = time.AfterFunc(ud.timerDuration, func() {\n\t\tud.mu.Lock()\n\t\tdefer ud.mu.Unlock()\n\t\tdelete(ud.fragments, ClientDstAddr)\n\t\tdelete(ud.highestFrag, ClientDstAddr)\n\t\tdelete(ud.timers, ClientDstAddr)\n\t})\n}\nfunc (ud *RelayUdpDirect) clientDstAddrAsID(clientAddr *net.UDPAddr, dstAddr string) string {\n\treturn fmt.Sprintf(\"%s/%s\", clientAddr, dstAddr)\n}\nfunc (ud *RelayUdpDirect) assembleThenPipeUdp(clientAddr *net.UDPAddr, dstAddr string) {\n\tvar data []byte\n\tclientDstAddr := ud.clientDstAddrAsID(clientAddr, dstAddr)\n\tfragments := ud.fragments[clientDstAddr]\n\t// Sort fragments by FRAG value.\n\tsort.Slice(fragments, func(i, j int) bool {\n\t\treturn fragments[i].Frag < fragments[j].Frag\n\t})\n\tfor _, frag := range fragments {\n\t\tdata = append(data, frag.Data...)\n\t}\n\tcomboPacket := fragments[0]\n\tcomboPacket.Data = data\n\n\t// Clean up after successful reassembly.\n\tdelete(ud.fragments, clientDstAddr)\n\tdelete(ud.highestFrag, clientDstAddr)\n\tif timer, exists := ud.timers[clientDstAddr]; exists {\n\t\ttimer.Stop()\n\t\tdelete(ud.timers, clientDstAddr)\n\t}\n\tud.segmentPipe(comboPacket, clientAddr)\n}\n\nfunc (ud *RelayUdpDirect) PipeUdp() {\n\tbuf := make([]byte, bufferSize)\n\tfor {\n\t\tn, clientAddr, err := ud.relayUdp.ReadFromUDP(buf)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn\n\t\t}\n\t\t//I will not verify the `clientAddr` because this SOCKS5 proxy is intended for local relay to bypass the GFW.\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error reading UDP data\", \"err\", err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tpacket, err := parseUDPData(buf[:n])\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error parsing UDP data\", err)\n\t\t\tcontinue\n\t\t}\n\t\tud.addFragment(clientAddr, packet)\n\t}\n}\n\nfunc (ud *RelayUdpDirect) segmentPipe(comboPacket *udpPacket, clientAddr *net.UDPAddr) {\n\tresp, err := forwardUDPData(comboPacket)\n\tif err != nil {\n\t\tslog.Error(\"Error forwarding UDP data\", \"err\", err.Error())\n\t\treturn\n\t}\n\theader := comboPacket.ResponseData(resp)\n\t_, err = ud.relayUdp.WriteToUDP(header, clientAddr)\n\tif err != nil {\n\t\tslog.Error(\"Error sending UDP response\", \"err\", err.Error())\n\t}\n}\n\nfunc NewRelayUdpDirect(s5 net.Conn) (*RelayUdpDirect, error) {\n\tudpAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}\n\tudpConn, err := net.ListenUDP(\"udp\", udpAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to bind UDP socket: %w\", err)\n\t}\n\tud := &RelayUdpDirect{\n\t\ts5:            s5,\n\t\trelayUdp:      udpConn,\n\t\tmu:            sync.Mutex{},\n\t\tfragments:     make(map[string][]*udpPacket),\n\t\thighestFrag:   make(map[string]byte),\n\t\ttimers:        make(map[string]*time.Timer),\n\t\ttimerDuration: time.Second * 60,\n\t}\n\n\tboundAddr := udpConn.LocalAddr().(*net.UDPAddr)\n\tresponse := []byte{\n\t\tsocks5Version, socks5ReplyOkay, socks5ReplyReserved, socks5AtypeIPv4,\n\t\tboundAddr.IP[0], boundAddr.IP[1], boundAddr.IP[2], boundAddr.IP[3],\n\t\tbyte(boundAddr.Port >> 8), byte(boundAddr.Port & 0xFF),\n\t}\n\t_, err = ud.s5.Write(response)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send response to client: %w\", err)\n\t}\n\n\treturn ud, nil\n}\n\nfunc forwardUDPData(udpPacket *udpPacket) ([]byte, error) {\n\tconn, err := net.DialUDP(\"udp\", nil, udpPacket.addr())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer conn.Close()\n\n\t_, err = conn.Write(udpPacket.Data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuf := make([]byte, bufferSize)\n\tn, _, err := conn.ReadFromUDP(buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf[:n], nil\n}\n\ntype udpPacket struct {\n\tRSV   [2]byte // reserved\n\tFrag  byte    // fragment\n\tAType byte    // dst address type\n\tAddr  []byte  // dst address\n\tPort  []byte  // dst port\n\tData  []byte  // payload\n}\n\nfunc (p udpPacket) ResponseData(payload []byte) []byte {\n\theader := []byte{p.RSV[0], p.RSV[1], 0, p.AType}\n\theader = append(header, p.Addr...)\n\theader = append(header, p.Port...)\n\treturn append(header, payload...)\n}\n\nfunc parseUDPData(data []byte) (*udpPacket, error) {\n\tif len(data) < 4 {\n\t\treturn nil, fmt.Errorf(\"invalid UDP packet\")\n\t}\n\t// parse header\n\tvar packet = udpPacket{\n\t\tRSV:   [2]byte{data[0], data[1]},\n\t\tFrag:  data[2],\n\t\tAType: data[3],\n\t}\n\tswitch packet.AType {\n\tcase socks5AtypeIPv4:\n\t\tif len(data) < 10 {\n\t\t\treturn nil, fmt.Errorf(\"invalid IPv4 UDP packet\")\n\t\t}\n\t\tpacket.Addr = data[4 : 4+net.IPv4len]\n\t\tpacket.Port = data[4+net.IPv4len : 4+net.IPv4len+2]\n\t\tpacket.Data = data[4+net.IPv4len+2:]\n\tcase socks5AtypeIPv6:\n\t\tif len(data) < 22 {\n\t\t\treturn nil, fmt.Errorf(\"invalid IPv6 UDP packet\")\n\t\t}\n\t\tpacket.Addr = data[4 : 4+net.IPv6len]\n\t\tpacket.Port = data[4+net.IPv6len : 4+net.IPv6len+2]\n\t\tpacket.Data = data[4+net.IPv6len+2:]\n\tcase socks5AtypeDomain:\n\t\tif len(data) < 7 {\n\t\t\treturn nil, fmt.Errorf(\"invalid domain UDP packet\")\n\t\t}\n\t\taddrLen := int(data[4])\n\t\tpacket.Addr = data[5 : 5+addrLen]\n\t\tpacket.Port = data[5+addrLen : 5+addrLen+2]\n\t\tpacket.Data = data[5+addrLen+2:]\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported address type: %d\", packet.AType)\n\t}\n\treturn &packet, nil\n}\n\nfunc (p udpPacket) ip() net.IP {\n\tswitch p.AType {\n\tcase socks5AtypeIPv4, socks5AtypeIPv6:\n\t\treturn p.Addr\n\tcase socks5AtypeDomain:\n\t\tips, err := net.LookupIP(string(p.Addr))\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to resolve domain\", \"err\", err.Error())\n\t\t\treturn net.IPv4zero\n\t\t}\n\t\tif len(ips) == 0 {\n\t\t\treturn net.IPv4zero\n\t\t}\n\t\treturn ips[0]\n\tdefault:\n\t\treturn net.IPv4zero\n\t}\n}\n\nfunc (p udpPacket) port() int {\n\treturn int(p.Port[0])<<8 + int(p.Port[1])\n}\nfunc (p udpPacket) addr() *net.UDPAddr {\n\treturn &net.UDPAddr{IP: p.ip(), Port: p.port()}\n}\nfunc (p udpPacket) dstAddr() string {\n\treturn fmt.Sprintf(\"%s:%d\", p.ip(), p.port())\n}\n\nfunc (ud *RelayUdpDirect) Close() {\n\t//s5 has already been closed in outside\n\tif ud.relayUdp != nil {\n\t\terr := ud.relayUdp.Close()\n\t\tif err != nil {\n\t\t\tlog.Println(\"close udp conn failed: \", err)\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "socks5ws/socks5_bind.go",
    "content": "package socks5ws\n\nimport (\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"sync\"\n)\n\nfunc relayBind(s5 net.Conn, _ *Socks5Request) {\n\tbindListener, err := net.Listen(\"tcp4\", \":0\")\n\tif err != nil {\n\t\tslog.Error(\"bind tcp failed\", \"err\", err)\n\t\tsocks5Response(s5, net.IPv4zero, 0, socks5ReplyFail)\n\t\treturn\n\t}\n\tdefer bindListener.Close()\n\t//first reply\n\tlocalAddr := bindListener.Addr().(*net.TCPAddr)\n\tsocks5Response(s5, localAddr.IP, localAddr.Port, socks5ReplyOkay)\n\n\ttargetConn, err := bindListener.Accept()\n\tif err != nil {\n\t\tslog.Error(\"bind tcp failed\", \"err\", err)\n\t\treturn\n\t}\n\tdefer targetConn.Close()\n\t//sec reply\n\ttargetAddr := targetConn.RemoteAddr().(*net.TCPAddr)\n\tsocks5Response(s5, targetAddr.IP, targetAddr.Port, socks5ReplyOkay)\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t_, err := io.Copy(targetConn, s5)\n\t\tif err != nil {\n\t\t\tslog.Error(\"bind tcp failed\", \"err\", err)\n\t\t}\n\t}()\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t_, err := io.Copy(s5, targetConn)\n\t\tif err != nil {\n\t\t\tslog.Error(\"bind tcp failed\", \"err\", err)\n\t\t}\n\t}()\n\twg.Wait()\n}\n"
  },
  {
    "path": "socks5ws/socks5_connect.go",
    "content": "package socks5ws\n"
  },
  {
    "path": "socks5ws/socks5_req.go",
    "content": "package socks5ws\n\nimport (\n\t\"fmt\"\n\t\"github.com/google/uuid\"\n\t\"github.com/mojocn/felix/util\"\n\t\"log/slog\"\n\t\"net\"\n)\n\ntype Socks5Request struct {\n\tid          string\n\tsocks5Cmd   byte\n\tsocks5Atyp  byte\n\tdstAddr     []byte\n\tdstPort     []byte\n\tCountryCode string //iso country code\n}\n\nfunc parseSocks5Request(data []byte, geo *util.GeoIP) (*Socks5Request, error) {\n\tid := uuid.NewString()\n\tinfo := &Socks5Request{id: id}\n\n\tif data[0] != socks5Version {\n\t\treturn nil, fmt.Errorf(\"unsupported SOCKS version: %d\", data[0])\n\t}\n\tif data[1] == socks5CmdConnect {\n\t\tinfo.socks5Cmd = socks5CmdConnect\n\t} else if data[1] == socks5CmdUdpAssoc {\n\t\tinfo.socks5Cmd = socks5CmdUdpAssoc\n\t} else {\n\t\t//BIND is not supported\n\t\treturn nil, fmt.Errorf(\"unsupported command: %d\", data[1])\n\t}\n\tif data[2] != socks5ReplyReserved {\n\t\treturn nil, fmt.Errorf(\"RSV must be 0x00\")\n\t}\n\tif data[3] == socks5AtypeIPv4 {\n\t\tif len(data) < 10 {\n\t\t\treturn nil, fmt.Errorf(\"request too short for atyp IPv4\")\n\t\t}\n\t\tinfo.socks5Atyp = socks5AtypeIPv4\n\t\tinfo.dstAddr = data[4:8]\n\t\tinfo.dstPort = data[8:10]\n\t} else if data[3] == socks5AtypeDomain {\n\t\tif len(data) < 5 {\n\t\t\treturn nil, fmt.Errorf(\"request too short for atyp Domain\")\n\t\t}\n\t\taddrLen := int(data[4])\n\t\tinfo.socks5Atyp = socks5AtypeDomain\n\t\tinfo.dstAddr = data[5 : 5+addrLen]\n\t\tinfo.dstPort = data[5+addrLen : 5+addrLen+2]\n\t} else if data[3] == socks5AtypeIPv6 {\n\t\tif len(data) < 22 {\n\t\t\treturn nil, fmt.Errorf(\"request too short for atyp IPv6\")\n\t\t}\n\t\tinfo.socks5Atyp = socks5AtypeIPv6\n\t\tinfo.dstAddr = data[4:20]\n\t\tinfo.dstPort = data[20:22]\n\t} else {\n\t\treturn nil, fmt.Errorf(\"unsupported address type: %d\", data[3])\n\t}\n\t//only get country code for connect command\n\tif info.socks5Cmd == socks5CmdConnect {\n\t\tcode, err := geo.Country(info.host())\n\t\tif err != nil {\n\t\t\tinfo.Logger().Error(\"failed to get country code\", \"err\", err.Error())\n\t\t} else {\n\t\t\tinfo.CountryCode = code\n\t\t}\n\t}\n\treturn info, nil\n}\n\nfunc (s Socks5Request) host() string {\n\taddr := \"\"\n\tif s.socks5Atyp == socks5AtypeIPv4 || s.socks5Atyp == socks5AtypeIPv6 {\n\t\taddr = net.IP(s.dstAddr).String()\n\t} else if s.socks5Atyp == socks5AtypeDomain {\n\t\taddr = string(s.dstAddr)\n\t} else {\n\t\taddr = string(s.dstAddr)\n\t}\n\treturn addr\n}\n\nfunc (s Socks5Request) addr() string {\n\treturn fmt.Sprintf(\"%s:%s\", s.host(), s.port())\n}\nfunc (s Socks5Request) cmd() string {\n\tcmd := \"unknown\"\n\tif s.socks5Cmd == socks5CmdConnect {\n\t\tcmd = \"connect\"\n\t} else if s.socks5Cmd == socks5CmdUdpAssoc {\n\t\tcmd = \"udp\"\n\t} else if s.socks5Cmd == socks5CmdBind {\n\t\tcmd = \"bind\"\n\t}\n\treturn cmd\n}\n\nfunc (s Socks5Request) Network() string {\n\tcmd := \"unknown\"\n\tif s.socks5Cmd == socks5CmdConnect {\n\t\tcmd = \"tcp\"\n\t} else if s.socks5Cmd == socks5CmdUdpAssoc {\n\t\tcmd = \"udp\"\n\t} else if s.socks5Cmd == socks5CmdBind {\n\t\tcmd = \"bind\"\n\t}\n\treturn cmd\n}\n\nfunc (s Socks5Request) aType() string {\n\treturn fmt.Sprintf(\"%v\", s.socks5Atyp)\n}\n\nfunc (s Socks5Request) port() string {\n\tport := int(s.dstPort[0])<<8 + int(s.dstPort[1])\n\treturn fmt.Sprintf(\"%v\", port)\n}\n\nfunc (s Socks5Request) Logger() *slog.Logger {\n\treturn slog.With(\"reqId\", s.id, \"cmd\", s.cmd(), \"atyp\", s.aType(), \"ip\", s.host(), \"port\", s.port(), \"country\", s.CountryCode)\n}\nfunc (s Socks5Request) String() string {\n\treturn fmt.Sprintf(\"socks5Cmd: %v, socks5Atyp: %v, dstAddr: %v, dstPort: %v, country: %s\", s.cmd(), s.aType(), s.host(), s.port(), s.CountryCode)\n}\n\nfunc (s Socks5Request) addressBytes() []byte {\n\tif s.socks5Atyp == socks5AtypeDomain {\n\t\treturn append([]byte{byte(len(s.dstAddr))}, s.dstAddr...)\n\t}\n\treturn s.dstAddr\n}\n"
  },
  {
    "path": "socks5ws/socks5_udp_associate.go",
    "content": "package socks5ws\n"
  },
  {
    "path": "socks5ws/websocket.go",
    "content": "package socks5ws\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/mojocn/felix/model\"\n\t\"log/slog\"\n\t\"net/http\"\n)\n\nconst (\n\tbrowserAgent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\"\n)\n\nfunc webSocketConn(ctx context.Context, proxy *model.Proxy, req *Socks5Request) (*websocket.Conn, error) {\n\twsDialer := websocket.DefaultDialer\n\n\theaders := http.Header{}\n\theaders.Set(\"Authorization\", proxy.UserID)\n\theaders.Set(\"User-Agent\", browserAgent)\n\tif proxy.Sni != \"\" {\n\t\theaders.Set(\"Host\", proxy.Sni)\n\t\twsDialer.TLSClientConfig = &tls.Config{\n\t\t\tServerName: proxy.Sni, // Set the SNI to the hostname of the server\n\t\t}\n\t}\n\theaders.Set(\"x-req-id\", req.id)\n\theaders.Set(\"x-dst-network\", \"tcp\")\n\theaders.Set(\"x-dst-addr\", req.host())\n\theaders.Set(\"x-dst-port\", req.port())\n\theaders.Set(\"x-dst-version\", \"socks5\") // socks5proxy.Version\n\turl := proxy.RelayURL()\n\tslog.Debug(\"connecting to remote proxy server\", \"url\", url)\n\tws, resp, err := websocket.DefaultDialer.DialContext(ctx, url, headers)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to connect to remote proxy server: %s ,error:%v\", proxy.RelayURL(), err)\n\t}\n\tif resp.StatusCode != http.StatusSwitchingProtocols {\n\t\treturn nil, fmt.Errorf(\"failed to connect to remote proxy server: %s ,error:%v\", proxy.RelayURL(), err)\n\t}\n\treturn ws, nil\n}\n"
  },
  {
    "path": "util/browse_open.go",
    "content": "package util\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n)\n\nfunc BrowserOpen(url string) error {\n\tvar cmd string\n\tvar args []string\n\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tcmd = \"cmd\"\n\t\targs = []string{\"/c\", \"start\"}\n\tcase \"darwin\":\n\t\tcmd = \"open\"\n\tdefault: // \"linux\", \"freebsd\", \"openbsd\", \"netbsd\"\n\t\tcmd = \"xdg-open\"\n\t}\n\targs = append(args, url)\n\treturn exec.Command(cmd, args...).Start()\n}\n"
  },
  {
    "path": "util/cf_ip.go",
    "content": "package util\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\n//https://api.cloudflare.com/client/v4/ips\n//https://api.cloudflare.com/client/v4/ips?networks=jdcloud\n\ntype CfIP struct {\n\tIpv4Cidrs    []string `json:\"ipv4_cidrs\"`\n\tIpv6Cidrs    []string `json:\"ipv6_cidrs\"`\n\tReachableIPs chan string\n}\n\nvar cfIpNode *CfIP\n\nfunc singletonCfIP() *CfIP {\n\tif cfIpNode == nil {\n\t\tvar err error\n\t\tcfIpNode, err = NewCfIP()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting cf ip: %v\\n\", err)\n\t\t}\n\t}\n\treturn cfIpNode\n}\n\nfunc NewCfIP() (*CfIP, error) {\n\tresp, err := http.Get(\"https://api.cloudflare.com/client/v4/ips\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get cf ip failed %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"get cf ip failed %s\", resp.Status)\n\t}\n\tdefer resp.Body.Close()\n\tall, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read cf ip failed %w\", err)\n\t}\n\tresult := struct {\n\t\tResult struct {\n\t\t\tIpv4Cidrs []string `json:\"ipv4_cidrs\"`\n\t\t\tIpv6Cidrs []string `json:\"ipv6_cidrs\"`\n\t\t\tEtag      string   `json:\"etag\"`\n\t\t} `json:\"result\"`\n\t\tSuccess  bool          `json:\"success\"`\n\t\tErrors   []interface{} `json:\"errors\"`\n\t\tMessages []interface{} `json:\"messages\"`\n\t}{}\n\terr = json.Unmarshal(all, &result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal cf ip failed %w\", err)\n\t}\n\n\treturn &CfIP{\n\t\tIpv4Cidrs:    result.Result.Ipv4Cidrs,\n\t\tIpv6Cidrs:    result.Result.Ipv4Cidrs,\n\t\tReachableIPs: make(chan string),\n\t}, nil\n}\n\nfunc (ci CfIP) IsCf(ip net.IP) bool {\n\tfor _, cidr := range append(ci.Ipv4Cidrs, ci.Ipv6Cidrs...) {\n\t\t_, ipNet, err := net.ParseCIDR(cidr)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error parsing Cidr: %v\\n\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif ipNet.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (ci CfIP) AllIps(fn func(ip, cidr string)) {\n\tfor _, cidr := range append(ci.Ipv4Cidrs, ci.Ipv6Cidrs...) {\n\t\tips, err := getIPsFromCIDR(cidr)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error parsing Cidr: %v\\n\", err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, ip := range ips {\n\t\t\tfn(ip, cidr)\n\t\t}\n\t}\n}\n\n//func (ci CfIP) CheckReachableIps() {\n//\tmaxWorkers := runtime.GOMAXPROCS(0) * 64\n//\tips := ci.ips()\n//\tjobs := make(chan string, len(ips))\n//\tresultChan := make(chan string, len(ips))\n//\tvar wg sync.WaitGroup\n//\tfor i := 0; i < maxWorkers; i++ {\n//\t\twg.Add(1)\n//\t\tgo func() {\n//\t\t\tdefer wg.Done()\n//\t\t\tfor ip := range jobs {\n//\t\t\t\tif isReachable(ip, 443) {\n//\t\t\t\t\tresultChan <- ip\n//\t\t\t\t}\n//\t\t\t}\n//\t\t}()\n//\t}\n//\tfor _, ip := range ci.ips() {\n//\t\tjobs <- ip\n//\t}\n//\tclose(jobs)\n//\twg.Wait()\n//\tvar reachable []string\n//\tfor ip := range resultChan {\n//\t\treachable = append(reachable, ip)\n//\t}\n//\tfd, err := os.Create(\"cf_reachable_ips.txt\")\n//\tif err != nil {\n//\t\tlog.Println(err)\n//\t\treturn\n//\t}\n//\tdefer fd.Close()\n//\tfd.Write([]byte(strings.Join(reachable, \"\\n\")))\n//}\n\nfunc getIPsFromCIDR(cidr string) ([]string, error) {\n\tip, ipNet, err := net.ParseCIDR(cidr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ips []string\n\tfor ip := ip.Mask(ipNet.Mask); ipNet.Contains(ip); inc(ip) {\n\t\tips = append(ips, ip.String())\n\t}\n\t// Remove network address and broadcast address\n\treturn ips[1 : len(ips)-1], nil\n}\n\nfunc inc(ip net.IP) {\n\tfor j := len(ip) - 1; j >= 0; j-- {\n\t\tip[j]++\n\t\tif ip[j] > 0 {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc isReachable(ip string, port int) bool {\n\ttimeout := time.Millisecond * 70\n\tconn, err := net.DialTimeout(\"tcp\", fmt.Sprintf(\"%s:%d\", ip, port), timeout)\n\tif err != nil {\n\t\tlog.Printf(\"Error connecting to %s:%d: %v\\n\", ip, port, err)\n\t\treturn false\n\t}\n\tconn.Close()\n\treturn true\n}\n"
  },
  {
    "path": "util/cf_ip_test.go",
    "content": "package util\n\nimport \"testing\"\n\nfunc TestCfIP_CheckReachableIps(t *testing.T) {\n\n}\n"
  },
  {
    "path": "util/crypt_aes.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"encoding/base64\"\n)\n\n//https://tech.mojotv.cn/2019/06/28/golang-crypt#svekr\n// key length must be 16/24/32\nfunc AesEncrypt(origData []byte, key string) (string, error) {\n\t// 转成字节数组\n\tk := []byte(key)\n\n\t// 分组秘钥\n\tblock, err := aes.NewCipher(k)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// 获取秘钥块的长度\n\tblockSize := block.BlockSize()\n\t// 补全码\n\torigData = PKCS7Padding(origData, blockSize)\n\t// 加密模式\n\tblockMode := cipher.NewCBCEncrypter(block, k[:blockSize])\n\t// 创建数组\n\tcryted := make([]byte, len(origData))\n\t// 加密\n\tblockMode.CryptBlocks(cryted, origData)\n\n\treturn base64.RawURLEncoding.EncodeToString(cryted), nil\n\n}\n\nfunc AesDecrypt(cryted string, key string) ([]byte, error) {\n\t// 转成字节数组\n\tcrytedByte, err := base64.RawURLEncoding.DecodeString(cryted)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tk := []byte(key)\n\n\t// 分组秘钥\n\tblock, err := aes.NewCipher(k)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// 获取秘钥块的长度\n\tblockSize := block.BlockSize()\n\t// 加密模式\n\tblockMode := cipher.NewCBCDecrypter(block, k[:blockSize])\n\t// 创建数组\n\torig := make([]byte, len(crytedByte))\n\t// 解密\n\tblockMode.CryptBlocks(orig, crytedByte)\n\t// 去补全码\n\torig = PKCS7UnPadding(orig)\n\treturn orig, nil\n}\n\n//补码\nfunc PKCS7Padding(ciphertext []byte, blocksize int) []byte {\n\tpadding := blocksize - len(ciphertext)%blocksize\n\tpadtext := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(ciphertext, padtext...)\n}\n\n//去码\nfunc PKCS7UnPadding(origData []byte) []byte {\n\tlength := len(origData)\n\tunpadding := int(origData[length-1])\n\treturn origData[:(length - unpadding)]\n}\n"
  },
  {
    "path": "util/crypt_aes_test.go",
    "content": "package util\n\nimport \"testing\"\n\nfunc TestAesDecryptEn(t *testing.T) {\n\tkey := RandStringWordC(32)\n\n\tmsg := RandomString(12)\n\n\tcode, err := AesEncrypt([]byte(msg), key)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\ttMsg, err := AesDecrypt(code, key)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif string(tMsg) != msg {\n\t\tt.Error(\"aes failed\")\n\t}\n}\n"
  },
  {
    "path": "util/crypt_des.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"crypto/des\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\nfunc ZeroPadding(ciphertext []byte, blockSize int) []byte {\n\tpadding := blockSize - len(ciphertext)%blockSize\n\tpadtext := bytes.Repeat([]byte{0}, padding)\n\treturn append(ciphertext, padtext...)\n}\n\nfunc ZeroUnPadding(origData []byte) []byte {\n\treturn bytes.TrimFunc(origData,\n\t\tfunc(r rune) bool {\n\t\t\treturn r == rune(0)\n\t\t})\n}\n\nfunc DesEncrypt(text string, key []byte) (string, error) {\n\tsrc := []byte(text)\n\tblock, err := des.NewCipher(key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbs := block.BlockSize()\n\tsrc = ZeroPadding(src, bs)\n\tif len(src)%bs != 0 {\n\t\treturn \"\", errors.New(\"Need a multiple of the blocksize\")\n\t}\n\tout := make([]byte, len(src))\n\tdst := out\n\tfor len(src) > 0 {\n\t\tblock.Encrypt(dst, src[:bs])\n\t\tsrc = src[bs:]\n\t\tdst = dst[bs:]\n\t}\n\treturn hex.EncodeToString(out), nil\n}\nfunc DesDecrypt(decrypted string, key []byte) (string, error) {\n\tsrc, err := hex.DecodeString(decrypted)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tblock, err := des.NewCipher(key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tout := make([]byte, len(src))\n\tdst := out\n\tbs := block.BlockSize()\n\tif len(src)%bs != 0 {\n\t\treturn \"\", errors.New(\"crypto/cipher: input not full blocks\")\n\t}\n\tfor len(src) > 0 {\n\t\tblock.Decrypt(dst, src[:bs])\n\t\tsrc = src[bs:]\n\t\tdst = dst[bs:]\n\t}\n\tout = ZeroUnPadding(out)\n\treturn string(out), nil\n}\n"
  },
  {
    "path": "util/enable_socks5_darwin.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nfunc proxySettingOn(socks5Addr string) {\n\tnetworkService := \"Wi-Fi\"   // Change this to your active network service name\n\tproxyAddress := \"127.0.0.1\" // SOCKS5 proxy address\n\tproxyPort := \"1080\"         // SOCKS5 proxy port\n\n\t// Disable the SOCKS proxy first (optional cleanup)\n\tdisableCmd := exec.Command(\"networksetup\", \"-setsocksfirewallproxystate\", networkService, \"off\")\n\tif err := disableCmd.Run(); err != nil {\n\t\tfmt.Printf(\"Failed to disable SOCKS proxy: %v\\n\", err)\n\t}\n\n\t// Set the SOCKS proxy\n\tcmd := exec.Command(\"networksetup\",\n\t\t\"-setsocksfirewallproxy\",\n\t\tnetworkService,\n\t\tproxyAddress,\n\t\tproxyPort)\n\tif err := cmd.Run(); err != nil {\n\t\tfmt.Printf(\"Failed to set SOCKS proxy: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Enable the SOCKS proxy\n\tenableCmd := exec.Command(\"networksetup\", \"-setsocksfirewallproxystate\", networkService, \"on\")\n\tif err := enableCmd.Run(); err != nil {\n\t\tfmt.Printf(\"Failed to enable SOCKS proxy: %v\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Println(\"SOCKS5 proxy configured successfully!\")\n}\n\nfunc proxySettingOff() (string, error) {\n\tcmd := exec.Command(\"networksetup\", \"-listallnetworkservices\")\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// List all network services\n\tservices := strings.Split(out.String(), \"\\n\")\n\tfor _, service := range services {\n\t\tif service != \"\" {\n\t\t\t// Check if the service is active by getting the status\n\t\t\tstatusCmd := exec.Command(\"networksetup\", \"-getinfo\", service)\n\t\t\tvar statusOut bytes.Buffer\n\t\t\tstatusCmd.Stdout = &statusOut\n\t\t\tif err := statusCmd.Run(); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\t// Check if the service has a valid IP address (active network)\n\t\t\tif strings.Contains(statusOut.String(), \"IP address\") {\n\t\t\t\treturn service, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"no active network service found\")\n}\n"
  },
  {
    "path": "util/enable_socks5_linux.go",
    "content": "package util\n\nfunc proxySettingOn(socks5Addr string) {\n\n}\nfunc proxySettingOff(socks5Addr string) {\n\n}\n"
  },
  {
    "path": "util/enable_socks5_windows.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"golang.org/x/sys/windows/registry\"\n\t\"log\"\n)\n\nfunc proxySettingOn(socks5Addr string) {\n\t// Open the registry key for proxy settings\n\tkey, err := registry.OpenKey(registry.CURRENT_USER, `Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings`, registry.SET_VALUE)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error opening registry key: %v\", err)\n\t}\n\tdefer key.Close()\n\n\t// Enable proxy and set proxy server to SOCKS5\n\terr = key.SetDWordValue(\"ProxyEnable\", 1) // 1 to enable proxy\n\tif err != nil {\n\t\tlog.Fatalf(\"Error enabling proxy: %v\", err)\n\t}\n\n\t// Set the SOCKS5 proxy address (e.g., \"socks=127.0.0.1:1080\")\n\terr = key.SetStringValue(\"ProxyServer\", \"socks5://\"+socks5Addr)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error setting proxy server: %v\", err)\n\t}\n\n\t// Set the proxy override settings : *.cn;*.local\n\t//\n\tskipAddrs := \"localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>;*.cn\"\n\terr = key.SetStringValue(\"ProxyOverride\", skipAddrs)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error setting proxy override: %v\", err)\n\t}\n\t// Optionally disable automatic proxy detection\n\terr = key.SetDWordValue(\"AutoDetect\", 0) // 0 to disable\n\tif err != nil {\n\t\tlog.Fatalf(\"Error disabling automatic proxy detection: %v\", err)\n\t}\n\n\tfmt.Println(\"SOCKS5 proxy configuration applied successfully.\")\n\n\t//cmd := exec.Command(\"netsh\", \"winhttp\", \"reset\", \"proxy\")\n\t//err = cmd.Run()\n\t//if err != nil {\n\t//\tslog.Error(\"Error resetting proxy settings: \", err)\n\t//}\n\t//log.Println(\"Network settings refreshed.\")\n}\nfunc proxySettingOff() {\n\tlog.Print(\"Disabling SOCKS5 proxy settings...\")\n\t// Open the registry key for proxy settings\n\tkey, err := registry.OpenKey(registry.CURRENT_USER, `Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings`, registry.SET_VALUE)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error opening registry key: %v\", err)\n\t}\n\tdefer key.Close()\n\n\t// Enable proxy and set proxy server to SOCKS5\n\terr = key.SetDWordValue(\"ProxyEnable\", 0) // 1 to enable proxy\n\tif err != nil {\n\t\tlog.Fatalf(\"Error enabling proxy: %v\", err)\n\t}\n\n\t//cmd := exec.Command(\"netsh\", \"winhttp\", \"reset\", \"proxy\")\n\t//err = cmd.Run()\n\t//if err != nil {\n\t//\tlog.Fatalf(\"Error resetting proxy settings: %v\", err)\n\t//}\n\t//log.Println(\"Network settings refreshed.\")\n}\n"
  },
  {
    "path": "util/geo_ip.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"github.com/oschwald/geoip2-golang\"\n\t\"net\"\n)\n\ntype GeoIP struct {\n\tdb *geoip2.Reader\n}\n\nfunc NewGeoIP(geoIpFilePath string) (*GeoIP, error) {\n\tif geoIpFilePath == \"\" {\n\t\tgeoIpFilePath = \"GeoLite2-Country.mmdb\" //https://github.com/P3TERX/GeoLite.mmdb?tab=readme-ov-file\n\t}\n\tdb, err := geoip2.Open(geoIpFilePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &GeoIP{db: db}, nil\n}\n\nfunc (g *GeoIP) Close() error {\n\tif g.db != nil {\n\t\treturn g.db.Close()\n\t}\n\treturn nil\n}\n\nfunc (g *GeoIP) Country(host string) (isoCountryCode string, err error) {\n\tif g == nil {\n\t\treturn \"\", fmt.Errorf(\"geo databse is nil\")\n\t}\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\tips, err := net.LookupIP(host)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to lookup IP: %w\", err)\n\t\t}\n\t\tif len(ips) == 0 {\n\t\t\treturn \"\", fmt.Errorf(\"no IP found for %s\", host)\n\t\t}\n\t\tip = ips[0]\n\t}\n\trecord, err := g.db.Country(ip)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get Country: %w\", err)\n\t}\n\treturn record.Country.IsoCode, nil\n}\n"
  },
  {
    "path": "util/geo_ip_test.go",
    "content": "package util\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGeoDns_country(t *testing.T) {\n\n}\n"
  },
  {
    "path": "util/random_string.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nvar (\n\tstringMisc = []byte(\".$#@&*_\")\n\n\tstringDigit    = []byte(\"1234567890\")\n\tstringDigitLen = len(stringDigit)\n\n\tstringLword    = []byte(\"abcdefghijklmnopqrstuvwxyz\")\n\tstringLwordLen = len(stringLword)\n\n\tstringUWord    = []byte(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\tstringUWordLen = len(stringUWord)\n\n\tstringCWord    = []byte(fmt.Sprintf(\"%s%s\", stringLword, stringUWord))\n\tstringCWordLen = len(stringCWord)\n\n\tletterRunes    = []byte(fmt.Sprintf(\"%s%s%s%s\", stringDigit, stringLword, stringMisc, stringUWord))\n\tletterRunesLen = len(letterRunes)\n)\n\nfunc RandomString(n int) string {\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tb[i] = letterRunes[rand.Intn(letterRunesLen)]\n\t}\n\treturn string(b)\n}\n\nfunc RandStringWordL(n int) string {\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tb[i] = stringLword[rand.Intn(stringLwordLen)]\n\t}\n\treturn string(b)\n}\n\nfunc RandStringWordU(n int) string {\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tb[i] = stringUWord[rand.Intn(stringUWordLen)]\n\t}\n\treturn string(b)\n}\nfunc RandStringWordC(n int) string {\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tb[i] = stringCWord[rand.Intn(stringCWordLen)]\n\t}\n\treturn string(b)\n}\nfunc RandStringDigit(n int) string {\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tb[i] = stringDigit[rand.Intn(stringDigitLen)]\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "util/vless_data.go",
    "content": "package util\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/google/uuid\"\n\t\"log/slog\"\n\t\"net\"\n)\n\ntype SchemaVLESS struct {\n\tuserID      uuid.UUID\n\tDstProtocol string //tcp or udp\n\tdstHost     string\n\tdstHostType string //ipv6 or ipv4,domain\n\tdstPort     uint16\n\tVersion     byte\n\tpayload     []byte\n}\n\nfunc (h SchemaVLESS) UUID() string {\n\treturn h.userID.String()\n}\n\nfunc (h SchemaVLESS) DataUdp() []byte {\n\tallData := make([]byte, 0)\n\tchunk := h.payload\n\tfor index := 0; index < len(chunk); {\n\t\tif index+2 > len(chunk) {\n\t\t\tfmt.Println(\"Incomplete length buffer\")\n\t\t\treturn nil\n\t\t}\n\t\tlengthBuffer := chunk[index : index+2]\n\t\tudpPacketLength := binary.BigEndian.Uint16(lengthBuffer)\n\t\tif index+2+int(udpPacketLength) > len(chunk) {\n\t\t\tfmt.Println(\"Incomplete UDP packet\")\n\t\t\treturn nil\n\t\t}\n\t\tudpData := chunk[index+2 : index+2+int(udpPacketLength)]\n\t\tindex = index + 2 + int(udpPacketLength)\n\t\tallData = append(allData, udpData...)\n\t}\n\treturn allData\n}\nfunc (h SchemaVLESS) DataTcp() []byte {\n\treturn h.payload\n}\n\nfunc (h SchemaVLESS) AddrUdp() *net.UDPAddr {\n\treturn &net.UDPAddr{IP: h.HostIP(), Port: int(h.dstPort)}\n}\nfunc (h SchemaVLESS) HostIP() net.IP {\n\tip := net.ParseIP(h.dstHost)\n\tif ip == nil {\n\t\tips, err := net.LookupIP(h.dstHost)\n\t\tif err != nil {\n\t\t\th.Logger().Error(\"failed to resolve domain\", \"err\", err.Error())\n\t\t\treturn net.IPv4zero\n\t\t}\n\t\tif len(ips) == 0 {\n\t\t\treturn net.IPv4zero\n\t\t}\n\t\treturn ips[0]\n\t}\n\treturn ip\n}\n\nfunc (h SchemaVLESS) HostPort() string {\n\treturn net.JoinHostPort(h.dstHost, fmt.Sprintf(\"%d\", h.dstPort))\n}\nfunc (h SchemaVLESS) Logger() *slog.Logger {\n\treturn slog.With(\"userID\", h.userID.String(), \"network\", h.DstProtocol, \"addr\", h.HostPort())\n}\n\n// VlessParse https://xtls.github.io/development/protocols/vless.html\nfunc VlessParse(buf []byte) (*SchemaVLESS, error) {\n\tpayload := &SchemaVLESS{\n\t\tuserID:      uuid.Nil,\n\t\tDstProtocol: \"\",\n\t\tdstHost:     \"\",\n\t\tdstPort:     0,\n\t\tVersion:     0,\n\t\tpayload:     nil,\n\t}\n\n\tif len(buf) < 24 {\n\t\treturn payload, errors.New(\"invalid payload length\")\n\t}\n\n\tpayload.Version = buf[0]\n\tpayload.userID = uuid.Must(uuid.FromBytes(buf[1:17]))\n\textraInfoProtoBufLen := buf[17]\n\n\tcommand := buf[18+extraInfoProtoBufLen]\n\tswitch command {\n\tcase 1:\n\t\tpayload.DstProtocol = \"tcp\"\n\tcase 2:\n\t\tpayload.DstProtocol = \"udp\"\n\tdefault:\n\t\treturn payload, fmt.Errorf(\"command %d is not supported, command 01-tcp, 02-udp, 03-mux\", command)\n\t}\n\n\tportIndex := 18 + extraInfoProtoBufLen + 1\n\tpayload.dstPort = binary.BigEndian.Uint16(buf[portIndex : portIndex+2])\n\n\taddressIndex := portIndex + 2\n\taddressType := buf[addressIndex]\n\taddressValueIndex := addressIndex + 1\n\n\tswitch addressType {\n\tcase 1: // IPv4\n\t\tif len(buf) < int(addressValueIndex+net.IPv4len) {\n\t\t\treturn nil, fmt.Errorf(\"invalid IPv4 address length\")\n\t\t}\n\t\tpayload.dstHost = net.IP(buf[addressValueIndex : addressValueIndex+net.IPv4len]).String()\n\t\tpayload.payload = buf[addressValueIndex+net.IPv4len:]\n\t\tpayload.dstHostType = \"ipv4\"\n\tcase 2: // domain\n\t\taddressLength := buf[addressValueIndex]\n\t\taddressValueIndex++\n\t\tif len(buf) < int(addressValueIndex)+int(addressLength) {\n\t\t\treturn nil, fmt.Errorf(\"invalid domain address length\")\n\t\t}\n\t\tpayload.dstHost = string(buf[addressValueIndex : int(addressValueIndex)+int(addressLength)])\n\t\tpayload.payload = buf[int(addressValueIndex)+int(addressLength):]\n\t\tpayload.dstHostType = \"domain\"\n\n\tcase 3: // IPv6\n\t\tif len(buf) < int(addressValueIndex+net.IPv6len) {\n\t\t\treturn nil, fmt.Errorf(\"invalid IPv6 address length\")\n\t\t}\n\t\tpayload.dstHost = net.IP(buf[addressValueIndex : addressValueIndex+net.IPv6len]).String()\n\t\tpayload.payload = buf[addressValueIndex+net.IPv6len:]\n\t\tpayload.dstHostType = \"ipv6\"\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"addressType %d is not supported\", addressType)\n\t}\n\n\treturn payload, nil\n}\n"
  }
]