[
  {
    "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: '16 13 * * 1'\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', 'ruby' ]\n        # Learn more about CodeQL language support at https://git.io/codeql-language-support\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": ".github/workflows/go-test.yml",
    "content": "name: Go Unit Tests\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  golang-test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n          cache: true\n\n      - name: Run tests\n        run: go test ./... -v\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n      commit:\n  push:\n    tags:\n      - v*\n\npermissions:\n  contents: write\n\njobs:\n  release:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest, macos-13]\n    steps:\n      - uses: actions/checkout@v4\n        if: github.event.inputs.commit != ''\n        with:\n          # checkout the commit if provided\n          ref: ${{ github.event.inputs.commit }}\n          # unshallow the repository to ensure all tags are available\n          fetch-depth: 0\n\n      - uses: actions/checkout@v4\n        if: github.event.inputs.commit == ''\n        with:\n          # checkout the tag if provided, otherwise checkout the current ref\n          ref: ${{ github.event.inputs.tag != '' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }}\n\n      # workaround for Pro feature https://goreleaser.com/customization/nightlies/\n      # create a dirty tag if the commit is not tagged\n      - name: Get dirty git tag\n        id: dirty_tag\n        if: github.event.inputs.commit != ''\n        shell: bash\n        run: echo \"tag=$(git tag --points-at HEAD | grep -q . || git describe --tags --always --abbrev=8 --dirty)\" >> \"$GITHUB_OUTPUT\"\n      - name: Set dirty git tag\n        if: steps.dirty_tag.outputs.tag != ''\n        run: git tag ${{ steps.dirty_tag.outputs.tag }}\n\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: 'go.mod'\n\n      - name: Setup yq\n        if: runner.os == 'Windows'\n        uses: dcarbone/install-yq-action@v1\n\n      # workaround for Pro feature https://goreleaser.com/customization/prebuilt/\n      # and the inability to run `goreleaser release --id ${matrix.os}`\n      - name: Copy goreleaser config to temp location\n        run: cp .goreleaser.yml ${{ runner.temp }}/.goreleaser.yml\n      # remove all builds except the one for the current OS\n      - name: Override builds in copied config\n        run: yq${{ runner.os == 'Windows' && '.exe' || '' }} -i eval '.builds |= map(select(.id == \"${{ matrix.os }}\"))' ${{ runner.temp }}/.goreleaser.yml\n\n      - uses: goreleaser/goreleaser-action@v6\n        with:\n          args: release --clean --config ${{ runner.temp }}/.goreleaser.yml\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "gopath\nbin\ncookies\nroutes.yaml\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\nbuilds:\n  - id: ubuntu-latest\n    main: ./cmd/gof5\n    goos: [linux]\n    goarch: [amd64]\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X main.Version=v{{ .Version }}\n    env:\n      - CGO_ENABLED=1\n\n  - id: windows-latest\n    main: ./cmd/gof5\n    goos: [windows]\n    goarch: [amd64]\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X main.Version=v{{ .Version }}\n    env:\n      - CGO_ENABLED=1\n\n  - id: macos-13\n    main: ./cmd/gof5\n    goos: [darwin]\n    goarch: [amd64]\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X main.Version=v{{ .Version }}\n    env:\n      - CGO_ENABLED=1\n\n  - id: macos-latest\n    main: ./cmd/gof5\n    goos: [darwin]\n    goarch: [arm64]\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X main.Version=v{{ .Version }}\n    env:\n      - CGO_ENABLED=1\n\narchives:\n  - formats: [binary]\n    name_template: \"{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}\"\n\nchecksum:\n  split: true\n\nrelease:\n  draft: true\n  use_existing_draft: true\n  replace_existing_draft: false\n\nchangelog:\n  disable: true\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 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": "PKG:=github.com/kayrus/gof5\nAPP_NAME:=gof5\nPWD:=$(shell pwd)\nUID:=$(shell id -u)\nVERSION:=$(shell git describe --tags --always --dirty=\"-dev\")\nGOOS:=$(shell go env GOOS)\nLDFLAGS:=-X main.Version=$(VERSION) -w -s\nGOOS:=$(strip $(shell go env GOOS))\nGOARCHs:=$(strip $(shell go env GOARCH))\n\nifeq \"$(GOOS)\" \"windows\"\nSUFFIX=.exe\nendif\n\n# CGO must be enabled\nexport CGO_ENABLED:=1\n\nbuild: fmt vet\n\t$(foreach GOARCH,$(GOARCHs),$(shell GOARCH=$(GOARCH) go build -ldflags=\"$(LDFLAGS)\" -trimpath -o bin/$(APP_NAME)_$(GOOS)_$(GOARCH)$(SUFFIX) ./cmd/gof5))\n\ndocker:\n\tdocker pull golang:latest\n\tdocker run -ti --rm -e GOCACHE=/tmp -v $(PWD):/$(APP_NAME) -u $(UID):$(UID) --workdir /$(APP_NAME) golang:latest make\n\nfmt:\n\tgofmt -s -w cmd pkg\n\nvet:\n\tgo vet ./...\n\nstatic:\n\tstaticcheck ./cmd/... ./pkg/...\n\ntest:\n\tgo test -v ./cmd/... ./pkg/...\n"
  },
  {
    "path": "README.md",
    "content": "# gof5\n\n## Requirements\n\n* an application must be executed under a privileged user\n\n## Linux\n\nIf your Linux distribution uses [systemd-resolved](https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html) or [NetworkManager](https://wiki.gnome.org/Projects/NetworkManager) you can run gof5 without sudo privileges.\nYou need to adjust the binary capabilities:\n\n```sh\n$ sudo setcap cap_net_admin,cap_net_bind_service+ep /path/to/binary/gof5\n```\n\nFor systemd-resolved you need to adjust PolicyKit Local Authority config, e.g. in Ubuntu:\n\n```sh\n$ cd gof5 # changedir to gof5 github repo\n$ sudo cp org.freedesktop.resolve1.pkla /var/lib/polkit-1/localauthority/50-local.d/org.freedesktop.resolve1.pkla\n$ sudo systemctl restart polkit.service\n```\n\n### Per user capabilities\n\nIf you want to have more granular restrictions to run gof5, you can allow only particular users to run it.\n\nFirst of all add an entry before the `none  *` in a `/etc/security/capability.conf` file:\n\n```\ncap_net_admin,cap_net_bind_service %username%\n```\n\nwhere a `%username%` is a name of the user, which should get inherited `CAP_NET_ADMIN` and `CAP_NET_BIND_SERVICE` capabilities.\n\nAdjust the binary flags to have inherited capabilities only:\n\n```\n$ sudo setcap cap_net_admin,cap_net_bind_service+i /path/to/binary/gof5\n```\n\nCheck user's capabilities:\n\n```\n$ sudo -u %username% capsh --print | awk '/Current/{print $NF}'\ncap_net_bind_service,cap_net_admin+i\n```\n\ngof5 should be executed using sudo even if you already logged in as this user:\n\n```\n$ sudo -u %username% /path/to/binary/gof5\n```\n\n## MacOS\n\nOn MacOS run the command below to avoid a `cannot be opened because the developer cannot be verified` warning:\n\n```sh\nxattr -d com.apple.quarantine ./path/to/gof5_darwin\n```\n\n## Windows\n\nWindows version doesn't support `pppd` driver.\n\n## ChromeOS\n\nDeveloper mode should be enabled, since gof5 requires root privileges.\nThe binary should be placed inside the `/usr/share/oem` directory. Home directory in ChromeOS doesn't allow to have executables.\nYou need to restart shill with an option in order to allow tun interface creation: `sudo restart shill BLOCKED_DEVICES=tun0`.\nUse the the `driver: pppd` config option if you don't want to restart shill.\n\n## HOWTO\n\n### Build from source\n\n```sh\n$ make # gmake in freebsd or mingw make for windows\n# or build inside docker (linux version only)\n$ make docker\n```\n\n### Run\n\n```sh\n# download the latest release\n$ sudo gof5 --server server --username username --password token\n```\n\nAlternatively you can use a session ID, obtained during the web browser authentication (in case, when you have MFA). You can find the session ID by going to the VPN host in a web browser, logging in, and running this JavaScript in Developer Tools:\n\n```js\ndocument.cookie.match(/MRHSession=(.*?); /)[1]\n```\n\nThen specify it as an argument:\n\n```sh\n$ sudo gof5 --server server --session sessionID\n```\n\nWhen username and password are not provided, they will be asked if `~/.gof5/cookies.yaml` file doesn't contain previously saved HTTPS session cookies or when the saved session is expired or explicitly terminated (`--close-session`).\n\nUse `--close-session` flag to terminate an HTTPS VPN session on exit. Next startup will require a valid username/password.\n\nUse `--select` to choose a VPN server from the list, known to a current server.\n\nUse `--profile-index` to define a custom F5 VPN profile index.\n\n### CA certificate and TLS keypair\n\nUse options below to specify custom TLS parameters:\n\n* `--ca-cert` - path to a custom CA certificate\n* `--cert` - path to a user TLS certificate\n* `--key` - path to a user TLS key\n\n## Configuration\n\nYou can define an extra `~/.gof5/config.yaml` file with contents:\n\n```yaml\n# DNS proxy listen address, defaults to 127.0.0.245\n# In BSD defaults to 127.0.0.1\n# listenDNS: 127.0.0.1\n# rewrite /etc/resolv.conf instead of renaming\n# Linux only, required in cases when /etc/resolv.conf cannot be renamed\nrewriteResolv: false\n# experimental DTLSv1.2 support\n# F5 BIG-IP server should have enabled DTLSv1.2 support\ndtls: false\n# TLS certificate check\ninsecureTLS: false\n# Enable IPv6\nipv6: false\n# driver specifies which tunnel driver to use.\n# supported values are: wireguard or pppd.\n# wireguard is default.\n# pppd requires a pppd or ppp (in FreeBSD) binary\ndriver: wireguard\n# When pppd driver is used, you can specify a list of extra pppd arguments\nPPPdArgs: []\n# disableDNS allows to completely disable DNS handling,\n# i.e. don't alter system DNS (e.g. /etc/resolv.conf) at all\ndisableDNS: false\n# TLS renegotiation support as defined in tls.RenegotiationSupport, disabled by default\nrenegotiation: RenegotiateNever\n# A list of DNS zones to be resolved by VPN DNS servers\n# When empty, every DNS query will be resolved by VPN DNS servers\ndns:\n- .corp.int.\n- .corp.\n# for reverse DNS lookup\n- .in-addr.arpa.\n# override DNS servers, provided by a VPN server profile\noverrideDNS:\n- 8.8.8.8\n# override DNS search suffix, provided by a VPN server profile\noverrideDNSSuffix:\n- my.corp\n# A list of subnets to be routed via VPN\n# When not set, the routes pushed from F5 will be used\n# Use \"routes: []\", if you don't want gof5 to manage routes at all\nroutes:\n- 1.2.3.4\n- 1.2.3.5/32\n```\n"
  },
  {
    "path": "SIGNATURE.md",
    "content": "# Signature\n\n* F5 client requests a token from a server: `/my.logon.php3?outform=xml&client_version=2.0&get_token=1`\n* F5 server sends a **token** to a client: `<?xml version=\"1.0\"?><data><token>1</token><version>2.0</version><redirect_url>/my.policy</redirect_url><max_client_data>16384</max_client_data></data>`\n* F5 client generates an **XML** with client parameters:\n\n```xml\n<agent_info>\n  <type>standalone</type>\n  <version>2.0</version>\n  <platform>Linux</platform>\n  <cpu>x64</cpu>\n  <javascript>no</javascript>\n  <activex>no</activex>\n  <plugin>no</plugin>\n  <landinguri>/</landinguri>\n  <lockedmode>no</lockedmode>\n  <hostname>dGVzdA==</hostname> // base64(\"test\")\n  <app_id/>\n</agent_info>\n```\n\nActual string:\n\n`<agent_info><type>standalone</type><version>2.0</version><platform>Linux</platform><cpu>x64</cpu><javascript>no</javascript><activex>no</activex><plugin>no</plugin><landinguri>/</landinguri><lockedmode>no</lockedmode><hostname>dGVzdA==</hostname><app_id></app_id></agent_info>`\n\n* then client generates some **signature** with 16 bytes size (HMAC-MD5 or a simple MD5) based on **token** and probably client's **useragent**. If **token** is spoofed to `1`, then the signature is `4sY+pQd3zrQ5c2Fl5BwkBg==` (base64([16]byte(\"e2c63ea50777ceb439736165e41c2406\")))\n* both **XML** and **signature** are base64 encoded and put into parameters:\n\n`client_data = sprintf(str, \"session=%s&device_info=%s&agent_result=%s&token=%s&signature=%s\", \"\", base64(xml), \"\", token, signature)`\n\n* The **client\\_data** string generated above is also base64 encoded and then sent as a POST request to F5 `/my.policy`:\n\n`post_request = sprintf(str, \"client_data=%s\", base64(client_data))`\n"
  },
  {
    "path": "cmd/gof5/gof5.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n<assemblyIdentity\n    version=\"6.0.0.0\"\n    name=\"org.kayrus.gof5\"\n    type=\"win32\"\n/>\n<description>gof5 requires Administrator privileges</description>\n<trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n        <requestedPrivileges>\n            <requestedExecutionLevel level=\"requireAdministrator\" uiAccess=\"false\"/>\n        </requestedPrivileges>\n    </security>\n</trustInfo>\n</assembly>\n"
  },
  {
    "path": "cmd/gof5/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/kayrus/gof5/pkg/client\"\n)\n\nvar (\n\tVersion = \"dev\"\n\tinfo    = fmt.Sprintf(\"gof5 %s compiled with %s for %s/%s\", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)\n)\n\nfunc fatal(err error) {\n\tif runtime.GOOS == \"windows\" {\n\t\t// Escalated privileges in windows opens a new terminal, and if there is an\n\t\t// error, it is impossible to see it. Thus we wait for user to press a button.\n\t\tlog.Printf(\"%s, press enter to exit\", err)\n\t\tbufio.NewReader(os.Stdin).ReadBytes('\\n')\n\t\tos.Exit(1)\n\t}\n\tlog.Fatal(err)\n}\n\nfunc main() {\n\tvar version bool\n\tvar opts client.Options\n\n\tflag.StringVar(&opts.Server, \"server\", \"\", \"\")\n\tflag.StringVar(&opts.Username, \"username\", \"\", \"\")\n\tflag.StringVar(&opts.Password, \"password\", \"\", \"\")\n\tflag.StringVar(&opts.SessionID, \"session\", \"\", \"Reuse a session ID\")\n\tflag.StringVar(&opts.CACert, \"ca-cert\", \"\", \"Path to a custom CA certificate\")\n\tflag.StringVar(&opts.Cert, \"cert\", \"\", \"Path to a user TLS certificate\")\n\tflag.StringVar(&opts.Key, \"key\", \"\", \"Path to a user TLS key\")\n\tflag.BoolVar(&opts.CloseSession, \"close-session\", false, \"Close HTTPS VPN session on exit\")\n\tflag.BoolVar(&opts.Debug, \"debug\", false, \"Show debug logs\")\n\tflag.BoolVar(&opts.Sel, \"select\", false, \"Select a server from available F5 servers\")\n\tflag.IntVar(&opts.ProfileIndex, \"profile-index\", 0, \"If multiple VPN profiles are found chose profile n\")\n\tflag.BoolVar(&version, \"version\", false, \"Show version and exit cleanly\")\n\n\tflag.Parse()\n\n\tif version {\n\t\tfmt.Println(info)\n\t\tos.Exit(0)\n\t}\n\n\tif opts.ProfileIndex < 0 {\n\t\tfatal(fmt.Errorf(\"profile-index cannot be negative\"))\n\t}\n\n\tlog.Print(info)\n\n\tif err := checkPermissions(); err != nil {\n\t\tfatal(err)\n\t}\n\n\tif flag.NArg() > 0 {\n\t\tif err := client.UrlHandlerF5Vpn(&opts, flag.Arg(0)); err != nil {\n\t\t\tfatal(err)\n\t\t}\n\t}\n\n\tif err := client.Connect(&opts); err != nil {\n\t\tfatal(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/gof5/root_linux.go",
    "content": "//go:build linux\n// +build linux\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"kernel.org/pub/linux/libs/security/libcap/cap\"\n)\n\nfunc checkCapability(c *cap.Set, capability cap.Value) error {\n\t// when \"setcap capability+ep gof5\" was used\n\tcapable, err := c.GetFlag(cap.Effective, capability)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get process effective capability flag: %v\", err)\n\t}\n\tif capable {\n\t\treturn nil\n\t}\n\n\t// when \"setcap capability+p gof5\" or \"setcap capability+i gof5\" was used and a user has inheritable capability\n\tcapable, err = c.GetFlag(cap.Permitted, capability)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get process permitted capability flag: %v\", err)\n\t}\n\tif capable {\n\t\tif err = c.SetFlag(cap.Effective, true, capability); err != nil {\n\t\t\treturn fmt.Errorf(\"permitted capability detected: failed to set effective %s capability flag: %v\", strings.ToUpper(capability.String()), err)\n\t\t}\n\t\tif err = c.SetProc(); err != nil {\n\t\t\treturn fmt.Errorf(\"permitted capability detected: failed to set effective %s capability: %v\", strings.ToUpper(capability.String()), err)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"cannot obtain effective %s capability\", strings.ToUpper(capability.String()))\n}\n\n// TODO: detect cap_net_bind_service for DNS bind\nfunc checkPermissions() error {\n\t// check root first\n\tif uid := os.Getuid(); uid == 0 {\n\t\treturn nil\n\t}\n\n\tc := cap.GetProc()\n\n\tvar err error\n\tcapabilities := []cap.Value{\n\t\tcap.NET_ADMIN, // to create and manage tun interface\n\t\t// no need to run own DNS proxy, when systemd-resolved is used\n\t\t// cap.NET_BIND_SERVICE, // to bind DNS proxy\n\t}\n\tfor _, capability := range capabilities {\n\t\terr = checkCapability(c, capability)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// no capabilities or \"setcap capability+i gof5\" was used and a user has no inheritable capability\n\treturn fmt.Errorf(\"gof5 needs to run with CAP_NET_ADMIN capability or as root: %v\", err)\n}\n"
  },
  {
    "path": "cmd/gof5/root_others.go",
    "content": "//go:build !windows && !linux\n// +build !windows,!linux\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc checkPermissions() error {\n\tif uid := os.Getuid(); uid != 0 {\n\t\treturn fmt.Errorf(\"gof5 needs to run as root\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/gof5/root_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc checkPermissions() error {\n\t// https://github.com/golang/go/issues/28804#issuecomment-505326268\n\tvar sid *windows.SID\n\n\t// https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership\n\terr := windows.AllocateAndInitializeSid(\n\t\t&windows.SECURITY_NT_AUTHORITY,\n\t\t2,\n\t\twindows.SECURITY_BUILTIN_DOMAIN_RID,\n\t\twindows.DOMAIN_ALIAS_RID_ADMINS,\n\t\t0, 0, 0, 0, 0, 0,\n\t\t&sid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while checking for elevated permissions: %s\", err)\n\t}\n\n\t// We must free the sid to prevent security token leaks\n\tdefer windows.FreeSid(sid)\n\ttoken := windows.Token(0)\n\n\tmember, err := token.IsMember(sid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while checking for elevated permissions: %s\", err)\n\t}\n\tif !member {\n\t\treturn fmt.Errorf(\"gof5 needs to run with administrator permissions\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/kayrus/gof5\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/IBM/netaddr v1.5.0\n\tgithub.com/fatih/color v1.10.0\n\tgithub.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c\n\tgithub.com/hpcloud/tail v1.0.0\n\tgithub.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382\n\tgithub.com/manifoldco/promptui v0.8.0\n\tgithub.com/miekg/dns v1.1.40\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/pion/dtls/v2 v2.2.4\n\tgithub.com/zaninime/go-hdlc v1.1.1\n\tgolang.org/x/net v0.47.0\n\tgolang.org/x/sys v0.38.0\n\tgopkg.in/yaml.v2 v2.4.0\n\tkernel.org/pub/linux/libs/security/libcap/cap v1.2.48\n)\n\nrequire (\n\tgithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect\n\tgithub.com/fsnotify/fsnotify v1.6.0 // indirect\n\tgithub.com/godbus/dbus/v5 v5.0.6 // indirect\n\tgithub.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect\n\tgithub.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect\n\tgithub.com/mattn/go-colorable v0.1.8 // indirect\n\tgithub.com/mattn/go-isatty v0.0.12 // indirect\n\tgithub.com/pion/logging v0.2.2 // indirect\n\tgithub.com/pion/transport/v2 v2.0.0 // indirect\n\tgithub.com/pion/udp v0.1.4 // indirect\n\tgithub.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162 // indirect\n\tgithub.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f // indirect\n\tgithub.com/vishvananda/netlink v1.1.0 // indirect\n\tgithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect\n\tgolang.org/x/crypto v0.45.0 // indirect\n\tgolang.org/x/term v0.37.0 // indirect\n\tgolang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71 // indirect\n\tgolang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a // indirect\n\tgopkg.in/fsnotify.v1 v1.4.7 // indirect\n\tgopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect\n\tkernel.org/pub/linux/libs/security/libcap/psx v1.2.48 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0=\ngithub.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4=\ngithub.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.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/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=\ngithub.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=\ngithub.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=\ngithub.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=\ngithub.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=\ngithub.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382 h1:FGitiUIfcFXN472O3leZ5+n1w97D6+R3xJZxRQRy9es=\ngithub.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382/go.mod h1:bU3N4PUqV+NW8pCT4gOS5Z7R1rqEq50q3vfP9hRhNj0=\ngithub.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=\ngithub.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=\ngithub.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=\ngithub.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=\ngithub.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=\ngithub.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=\ngithub.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=\ngithub.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=\ngithub.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=\ngithub.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=\ngithub.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=\ngithub.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=\ngithub.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=\ngithub.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=\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/sigurn/crc16 v0.0.0-20160107003519-da416fad5162 h1:2zlAtlrum6lg2lMiUWznq04fDudBDajMFl94Zyis67Y=\ngithub.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=\ngithub.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f h1:fKe0QdNJw68NO8iUdbC+jlwaA7/pA8sw0caZkpeXFTc=\ngithub.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f/go.mod h1:VRI4lXkrUH5Cygl6mbG1BRUfMMoT2o8BkrtBDUAm+GU=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=\ngithub.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=\ngithub.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/zaninime/go-hdlc v1.1.1 h1:L0NBRiv49mSsCC+oSEmTbAcUntr8nseJpC+6pwYkBZ0=\ngithub.com/zaninime/go-hdlc v1.1.1/go.mod h1:u/pMQOkSk+AucNZiuoil1ZKuO510qk8jn1JRyO7GR5w=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=\ngolang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=\ngolang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=\ngolang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=\ngolang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=\ngolang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=\ngolang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71 h1:xANEpH9Q0hSSf/ogUZZg9yPBxo2x9Js+7LZoHI/EJRE=\ngolang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71/go.mod h1:RTjaYEQboNk7+2qfPGBotaMEh/5HIvmPZ6DIe10lTqI=\ngolang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a h1:4+nkXW+gJ+wq7ZqAspB4hAQymenjGwgN4O/IHn+kTm0=\ngolang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a/go.mod h1:EC9wJeih/xJQBE9kSNihR8I0WmojSTQRQMsCLMpzqYY=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nkernel.org/pub/linux/libs/security/libcap/cap v1.2.48 h1:gW8VCEsPUwAp0/cW8CN2zfoqvz0+ijagsH2x+O2KlMM=\nkernel.org/pub/linux/libs/security/libcap/cap v1.2.48/go.mod h1:cs/AYPYd93hM59y4VPzpn4FP5TFgFoCcKtzlb0LM1c8=\nkernel.org/pub/linux/libs/security/libcap/psx v1.2.48 h1:5Oh8T4MP1+3KV2SvCBkCeGd97g7QHWMkTS7SrEme2bA=\nkernel.org/pub/linux/libs/security/libcap/psx v1.2.48/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=\n"
  },
  {
    "path": "org.freedesktop.resolve1.pkla",
    "content": "[Adding or changing system-wide resolved]\nIdentity=unix-group:netdev;unix-group:sudo\nAction=org.freedesktop.resolve1.*\nResultAny=no\nResultInactive=no\nResultActive=yes\n"
  },
  {
    "path": "pkg/client/client.go",
    "content": "package client\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"syscall\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n\t\"github.com/kayrus/gof5/pkg/cookie\"\n\t\"github.com/kayrus/gof5/pkg/link\"\n)\n\ntype Options struct {\n\tconfig.Config\n\tServer        string\n\tUsername      string\n\tPassword      string\n\tSessionID     string\n\tCACert        string\n\tCert          string\n\tKey           string\n\tCloseSession  bool\n\tDebug         bool\n\tSel           bool\n\tVersion       bool\n\tProfileIndex  int\n\tProfileName   string\n\tRenegotiation tls.RenegotiationSupport\n}\n\nfunc UrlHandlerF5Vpn(opts *Options, s string) error {\n\tu, err := url.Parse(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif u.Scheme != \"f5-vpn\" {\n\t\treturn fmt.Errorf(\"invalid scheme %v expected f5-vpn\", u.Scheme)\n\t}\n\n\tm, err := url.ParseQuery(u.RawQuery)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresourceTypes := m[\"resourcetype\"]\n\tresourceNames := m[\"resourcename\"]\n\tif len(resourceTypes) == len(resourceNames) {\n\t\tfor i := range resourceTypes {\n\t\t\tif resourceTypes[i] == \"network_access\" {\n\t\t\t\topts.ProfileName = resourceNames[i]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\topts.Server = m[\"server\"][0]\n\ttokenUrl := fmt.Sprintf(\"%s://%s:%s/vdesk/get_sessid_for_token.php3\", m[\"protocol\"][0], opts.Server, m[\"port\"][0])\n\trequest, err := http.NewRequest(http.MethodGet, tokenUrl, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\totc := m[\"otc\"]\n\trequest.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trequest.Header.Add(\"X-Access-Session-Token\", otc[len(otc)-1])\n\n\tresponse, err := http.DefaultClient.Do(request)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\topts.SessionID = response.Header.Get(\"X-Access-Session-ID\")\n\treturn nil\n}\n\nfunc Connect(opts *Options) error {\n\tif opts.Server == \"\" {\n\t\tfmt.Print(\"Enter server address: \")\n\t\tfmt.Scanln(&opts.Server)\n\t}\n\n\tu, err := url.Parse(opts.Server)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse server hostname: %s\", err)\n\t}\n\tif u.Scheme != \"https\" {\n\t\tu, err = url.Parse(fmt.Sprintf(\"https://%s\", u.Host))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse server hostname: %s\", err)\n\t\t}\n\t}\n\tif u.Host == \"\" {\n\t\tu, err = url.Parse(fmt.Sprintf(\"https://%s\", opts.Server))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse server hostname: %s\", err)\n\t\t}\n\t\tif u.Host == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to parse server hostname: %s\", err)\n\t\t}\n\t}\n\topts.Server = u.Host\n\n\t// read config\n\tcfg, err := config.ReadConfig(opts.Debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\topts.Config = *cfg\n\n\tswitch cfg.Renegotiation {\n\tcase \"RenegotiateOnceAsClient\":\n\t\topts.Renegotiation = tls.RenegotiateOnceAsClient\n\tcase \"RenegotiateFreelyAsClient\":\n\t\topts.Renegotiation = tls.RenegotiateFreelyAsClient\n\tcase \"RenegotiateNever\", \"\":\n\t\topts.Renegotiation = tls.RenegotiateNever\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown renegotiation value: '%s'\", cfg.Renegotiation)\n\t}\n\n\tcookieJar, err := cookiejar.New(nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create cookie jar: %s\", err)\n\t}\n\n\tclient := &http.Client{Jar: cookieJar}\n\tclient.CheckRedirect = checkRedirect(client)\n\n\ttlsConf, err := tlsConfig(opts, cfg.InsecureTLS)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to build TLS config: %v\", err)\n\t}\n\ttransport := &http.Transport{\n\t\tTLSClientConfig: tlsConf,\n\t}\n\tif opts.Debug {\n\t\tclient.Transport = &RoundTripper{\n\t\t\tRt:     transport,\n\t\t\tLogger: &logger{},\n\t\t}\n\t} else {\n\t\tclient.Transport = transport\n\t}\n\n\t// when server select list has been chosen\n\tif opts.Sel {\n\t\tu, err = getServersList(client, opts.Server)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\topts.Server = u.Host\n\t}\n\n\t// read cookies\n\tcookie.ReadCookies(client, u, cfg, opts.SessionID)\n\n\tif len(client.Jar.Cookies(u)) == 0 {\n\t\t// need to login\n\t\tif err := login(client, opts.Server, &opts.Username, &opts.Password); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to login: %s\", err)\n\t\t}\n\t} else {\n\t\tlog.Printf(\"Reusing saved HTTPS VPN session for %s\", u.Host)\n\t}\n\n\tresp, err := getProfiles(client, opts.Server)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get VPN profiles: %s\", err)\n\t}\n\n\tif resp.StatusCode == 302 {\n\t\t// need to relogin\n\t\t_, err = io.Copy(ioutil.Discard, resp.Body)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read response body: %s\", err)\n\t\t}\n\t\tresp.Body.Close()\n\n\t\tif err := login(client, opts.Server, &opts.Username, &opts.Password); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to login: %s\", err)\n\t\t}\n\n\t\t// new request\n\t\tresp, err = getProfiles(client, opts.Server)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get VPN profiles: %s\", err)\n\t\t}\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"wrong response code on profiles get: %d\", resp.StatusCode)\n\t}\n\n\tprofile, err := parseProfile(resp.Body, opts.ProfileIndex, opts.ProfileName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse VPN profiles: %s\", err)\n\t}\n\n\t// read config, returned by F5\n\tcfg.F5Config, err = getConnectionOptions(client, opts, profile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get VPN connection options: %s\", err)\n\t}\n\n\t// save cookies\n\tif err := cookie.SaveCookies(client, u, cfg); err != nil {\n\t\treturn fmt.Errorf(\"failed to save cookies: %s\", err)\n\t}\n\n\t// close HTTPS VPN session\n\t// next VPN connection will require credentials to auth\n\tif opts.CloseSession {\n\t\tdefer closeVPNSession(client, opts.Server)\n\t}\n\n\t// TLS\n\tl, err := link.InitConnection(opts.Server, cfg, tlsConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer l.HTTPConn.Close()\n\n\tcmd := link.Cmd(cfg)\n\n\ttermChan := make(chan os.Signal, 1)\n\tsignal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE, syscall.SIGHUP)\n\n\t// set routes and DNS after the PPP/TUN is up\n\tgo l.WaitAndConfig(cfg)\n\n\t// 1. stop ppp/pppd child at the very end\n\tdefer l.StopPPPDChild(cmd)\n\t// 0. restore the config first\n\tdefer l.RestoreConfig(cfg)\n\n\tif cfg.Driver == \"pppd\" {\n\t\tif runtime.GOOS == \"freebsd\" {\n\t\t\t// ppp log parser\n\t\t\tgo l.PppLogParser()\n\t\t} else {\n\t\t\t/*\n\t\t\t\t// read file descriptor 3\n\t\t\t\tstderr, w, err := os.Pipe()\n\t\t\t\tcmd.ExtraFiles = []*os.File{w}\n\t\t\t*/\n\t\t\tstderr, err := cmd.StderrPipe()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot allocate stderr pipe: %s\", err)\n\t\t\t}\n\t\t\t// pppd log parser\n\t\t\tgo l.PppdLogParser(stderr)\n\t\t}\n\n\t\tstdin, err := cmd.StdinPipe()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot allocate stdin pipe: %s\", err)\n\t\t}\n\t\tstdout, err := cmd.StdoutPipe()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot allocate stdout pipe: %s\", err)\n\t\t}\n\n\t\terr = cmd.Start()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to start pppd: %s\", err)\n\t\t}\n\n\t\t// catch ppp/pppd child termination\n\t\tgo l.CatchPPPDTermination(cmd)\n\n\t\t// pppd http->tun go routine\n\t\tgo l.PppdHTTPToTun(stdin)\n\n\t\t// pppd tun->http go routine\n\t\tgo l.PppdTunToHTTP(stdout)\n\t} else {\n\t\t// http->tun go routine\n\t\tgo l.HttpToTun()\n\n\t\t// tun->http go routine\n\t\tgo l.TunToHTTP()\n\t}\n\n\tselect {\n\tcase sig := <-termChan:\n\t\tlog.Printf(\"received %s signal, exiting\", sig)\n\tcase err = <-l.ErrChan:\n\t\t// error received\n\tcase err = <-l.PppdErrChan:\n\t\t// ppp/pppd child error received\n\t}\n\n\t// notify tun readers and writes to stop\n\tclose(l.TunDown)\n\n\treturn err\n}\n"
  },
  {
    "path": "pkg/client/http.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n\n\t\"github.com/howeyc/gopass\"\n\t\"github.com/manifoldco/promptui\"\n\t\"github.com/mitchellh/go-homedir\"\n)\n\nconst (\n\tuserAgent        = \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1a2pre) Gecko/2008073000 Shredder/3.0a2pre ThunderBrowse/3.2.1.8\"\n\tandroidUserAgent = \"Mozilla/5.0 (Linux; Android 10; SM-G975F Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.138 Mobile Safari/537.36 EdgeClient/3.0.7 F5Access/3.0.7\"\n)\n\nfunc tlsConfig(opts *Options, insecure bool) (*tls.Config, error) {\n\tconfig := &tls.Config{\n\t\tInsecureSkipVerify: insecure,\n\t\tRenegotiation:      opts.Renegotiation,\n\t}\n\n\tif opts.CACert != \"\" {\n\t\tcaCert, err := readFile(opts.CACert)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tconfig.RootCAs = x509.NewCertPool()\n\t\tconfig.RootCAs.AppendCertsFromPEM(caCert)\n\t}\n\n\tif opts.Cert != \"\" && opts.Key != \"\" {\n\t\tcrt, err := readFile(opts.Cert)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkey, err := readFile(opts.Key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcert, err := tls.X509KeyPair(crt, key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tconfig.Certificates = []tls.Certificate{cert}\n\t}\n\n\treturn config, nil\n}\n\nfunc readFile(path string) ([]byte, error) {\n\tif len(path) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tif path[0] == '~' {\n\t\tvar err error\n\t\tpath, err = homedir.Expand(path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif _, err := os.Stat(path); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcontent, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn bytes.TrimSpace(content), nil\n}\n\nfunc checkRedirect(c *http.Client) func(*http.Request, []*http.Request) error {\n\treturn func(req *http.Request, via []*http.Request) error {\n\t\tif req.URL.Path == \"/my.logout.php3\" || req.URL.Path == \"/vdesk/hangup.php3\" || req.URL.Query().Get(\"errorcode\") != \"\" {\n\t\t\t// clear cookies\n\t\t\tvar err error\n\t\t\tc.Jar, err = cookiejar.New(nil)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create cookie jar: %s\", err)\n\t\t\t}\n\t\t\treturn http.ErrUseLastResponse\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// 64-byte HMAC key\nvar hmacKey, _ = hex.DecodeString(\n\t\"4342a2ee5e546d98bd24e014218c8b8d\" +\n\t\"c18531bd538c4694b720043435367edb\" +\n\t\"f5dd67a9f6da42b58d28b27710c39b1a\" +\n\t\"b4cb386acdae4e08bd328d8a45b0b082\")\n\nfunc generateClientData(cData config.ClientData) (string, error) {\n\tinfo := config.AgentInfo{\n\t\tType:       \"standalone\",\n\t\tVersion:    \"2.0\",\n\t\tPlatform:   \"Linux\",\n\t\tCPU:        \"x64\",\n\t\tLandingURI: \"/\",\n\t\tHostname:   \"test\",\n\t}\n\n\tdata, err := xml.Marshal(info)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal agent info: %s\", err)\n\t}\n\n\tif info.AppID == \"\" {\n\t\tr := regexp.MustCompile(\"></agent_info>\")\n\t\tdata = []byte(r.ReplaceAllString(string(data), \"><app_id></app_id></agent_info>\"))\n\t}\n\n\tvalues := &bytes.Buffer{}\n\tvalues.WriteString(\"session=&\")\n\tvalues.WriteString(\"device_info=\" + base64.StdEncoding.EncodeToString(data) + \"&\")\n\tvalues.WriteString(\"agent_result=&\")\n\tvalues.WriteString(\"token=\" + cData.Token)\n\n\t// HMAC-MD5 with the 64-byte key\n\th := hmac.New(md5.New, hmacKey)\n\th.Write(values.Bytes())\n\tsig := base64.StdEncoding.EncodeToString(h.Sum(nil))\n\n\tvalues.WriteString(\"&signature=\" + sig)\n\n\treturn base64.StdEncoding.EncodeToString(values.Bytes()), nil\n}\n\nfunc loginSignature(c *http.Client, server string, _, _ *string) error {\n\tlog.Printf(\"Logging in...\")\n\treq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://%s/my.logon.php3?outform=xml&client_version=2.0&get_token=1\", server), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Proto = \"HTTP/1.0\"\n\treq.Header.Set(\"User-Agent\", androidUserAgent)\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar cData config.ClientData\n\tdec := xml.NewDecoder(resp.Body)\n\terr = dec.Decode(&cData)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclientData, err := generateClientData(cData)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err = http.NewRequest(\"POST\", fmt.Sprintf(\"https://%s%s\", server, cData.RedirectURL), strings.NewReader(\"client_data=\"+clientData))\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"User-Agent\", androidUserAgent)\n\treq.Header.Set(\"Pragma\", \"no-cache\")\n\treq.Header.Set(\"Cache-Control\", \"no-cache\")\n\treq.Header.Set(\"Upgrade-Insecure-Requests\", \"1\")\n\treq.Header.Set(\"Origin\", \"null\")\n\treq.Header.Set(\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\")\n\treq.Header.Set(\"content-type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"X-Requested-With\", \"com.f5.edge.client_ics\")\n\treq.Header.Set(\"Sec-Fetch-Site\", \"none\")\n\treq.Header.Set(\"Sec-Fetch-Mode\", \"navigate\")\n\treq.Header.Set(\"Sec-Fetch-User\", \"?1\")\n\treq.Header.Set(\"Sec-Fetch-Dest\", \"document\")\n\treq.Header.Set(\"Accept-Encoding\", \"gzip, deflate\")\n\treq.Header.Set(\"Accept-Language\", \"en-US;q=0.9,en;q=0.8\")\n\n\tresp, err = c.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode == 302 {\n\t\treturn fmt.Errorf(\"login failed\")\n\t}\n\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc login(c *http.Client, server string, username, password *string) error {\n\tif *username == \"\" {\n\t\tfmt.Print(\"Enter VPN username: \")\n\t\tfmt.Scanln(username)\n\t}\n\tif *password == \"\" {\n\t\tfmt.Print(\"Enter VPN password: \")\n\t\tv, err := gopass.GetPasswd()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read password: %s\", err)\n\t\t}\n\t\t*password = string(v)\n\t}\n\n\tlog.Printf(\"Logging in...\")\n\treq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://%s\", server), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Proto = \"HTTP/1.0\"\n\treq.Header.Set(\"User-Agent\", userAgent)\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp.Body.Close()\n\n\tdata := url.Values{}\n\tdata.Set(\"username\", *username)\n\tdata.Add(\"password\", *password)\n\tdata.Add(\"vhost\", \"standard\")\n\treq, err = http.NewRequest(\"POST\", fmt.Sprintf(\"https://%s/my.policy?outform=xml\", server), strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Referer\", fmt.Sprintf(\"https://%s/my.policy\", server))\n\treq.Header.Set(\"User-Agent\", userAgent)\n\tresp, err = c.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp.Body.Close()\n\n\t/*\n\t\tif resp.StatusCode == 302 && resp.Header.Get(\"Location\") == \"/my.policy\" {\n\t\t\treturn nil\n\t\t}\n\t*/\n\n\t// TODO: parse response 302 location and error code\n\tif resp.StatusCode == 302 || bytes.Contains(body, []byte(\"Session Expired/Timeout\")) || bytes.Contains(body, []byte(\"The username or password is not correct\")) {\n\t\treturn fmt.Errorf(\"wrong credentials\")\n\t}\n\n\treturn nil\n}\n\nfunc parseProfile(reader io.ReadCloser, profileIndex int, profileName string) (string, error) {\n\tvar profiles config.Profiles\n\tdec := xml.NewDecoder(reader)\n\terr := dec.Decode(&profiles)\n\treader.Close()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal a response: %s\", err)\n\t}\n\n\tif profiles.Type == \"VPN\" {\n\t\tprfls := make([]string, len(profiles.Favorites))\n\t\tfor i, p := range profiles.Favorites {\n\t\t\tif profileName != \"\" && profileName == p.Name {\n\t\t\t\tprofileIndex = i\n\t\t\t}\n\t\t\tprfls[i] = fmt.Sprintf(\"%d:%s\", i, p.Name)\n\t\t}\n\t\tlog.Printf(\"Found F5 VPN profiles: %q\", prfls)\n\n\t\tif profileIndex >= len(profiles.Favorites) {\n\t\t\treturn \"\", fmt.Errorf(\"profile %q index is out of range\", profileIndex)\n\t\t}\n\t\tlog.Printf(\"Using %q F5 VPN profile\", profiles.Favorites[profileIndex].Name)\n\t\treturn profiles.Favorites[profileIndex].Params, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"VPN profile was not found\")\n}\n\nfunc getProfiles(c *http.Client, server string) (*http.Response, error) {\n\treq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://%s/vdesk/vpn/index.php3?outform=xml&client_version=2.0\", server), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to build a request: %s\", err)\n\t}\n\treq.Header.Set(\"User-Agent\", userAgent)\n\treturn c.Do(req)\n}\n\nfunc getConnectionOptions(c *http.Client, opts *Options, profile string) (*config.Favorite, error) {\n\treq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://%s/vdesk/vpn/connect.php3?%s&outform=xml&client_version=2.0\", opts.Server, profile), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to build a request: %s\", err)\n\t}\n\treq.Header.Set(\"User-Agent\", userAgent)\n\tresp, err := c.Do(req)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to read a request: %s\", err)\n\t\tlog.Printf(\"Override link DNS values from config\")\n\t\treturn &config.Favorite{\n\t\t\tObject: config.Object{\n\t\t\t\tSessionID: opts.SessionID,\n\t\t\t\tDNS:       opts.Config.OverrideDNS,\n\t\t\t\tDNSSuffix: opts.Config.OverrideDNSSuffix,\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// parse profile\n\tvar favorite config.Favorite\n\tdec := xml.NewDecoder(resp.Body)\n\terr = dec.Decode(&favorite)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal a response: %s\", err)\n\t}\n\n\t// override link options\n\tif favorite.Object.SessionID == \"\" {\n\t\tfavorite.Object.SessionID = opts.SessionID\n\t}\n\tif len(opts.Config.OverrideDNS) > 0 {\n\t\tfavorite.Object.DNS = opts.Config.OverrideDNS\n\t}\n\tif len(opts.Config.OverrideDNSSuffix) > 0 {\n\t\tfavorite.Object.DNSSuffix = opts.Config.OverrideDNSSuffix\n\t}\n\n\treturn &favorite, nil\n}\n\nfunc closeVPNSession(c *http.Client, server string) {\n\t// close session\n\tr, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://%s/vdesk/hangup.php3?hangup_error=1\", server), nil)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to create a request to close the VPN session %s\", err)\n\t}\n\tresp, err := c.Do(r)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to close the VPN session %s\", err)\n\t}\n\tdefer resp.Body.Close()\n}\n\nfunc getServersList(c *http.Client, server string) (*url.URL, error) {\n\tr, err := http.NewRequest(\"GET\", fmt.Sprintf(\"https://%s/pre/config.php\", server), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create a request to get servers list: %s\", err)\n\t}\n\tresp, err := c.Do(r)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to request servers list: %s\", err)\n\t}\n\n\tvar s config.PreConfigProfile\n\tdec := xml.NewDecoder(resp.Body)\n\terr = dec.Decode(&s)\n\tresp.Body.Close()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal servers list: %s\", err)\n\t}\n\n\tprompt := promptui.Select{\n\t\tLabel: \"Select Server\",\n\t\tItems: s.Servers,\n\t}\n\n\ti, _, err := prompt.Run()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"prompt failed: %s\", err)\n\t}\n\n\tu, err := url.Parse(s.Servers[i].Address)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse server hostname: %s\", err)\n\t}\n\n\t// if scheme is not set, assume https\n\tif u.Scheme == \"\" {\n\t\tu, err = url.Parse(\"https://\" + s.Servers[i].Address)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse server hostname: %s\", err)\n\t\t}\n\t}\n\n\treturn u, nil\n}\n"
  },
  {
    "path": "pkg/client/http_test.go",
    "content": "package client\n\nimport (\n\t\"encoding/xml\"\n\t\"testing\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n)\n\nfunc TestSignature(t *testing.T) {\n\ts, err := generateClientData(config.ClientData{Token: \"1\"})\n\tif err != nil {\n\t\tt.Errorf(\"Signature is wrong: %s\", err)\n\t}\n\n\texpected := \"c2Vzc2lvbj0mZGV2aWNlX2luZm89UEdGblpXNTBYMmx1Wm04K1BIUjVjR1UrYzNSaGJtUmhiRzl1WlR3dmRIbHdaVDQ4ZG1WeWMybHZiajR5TGpBOEwzWmxjbk5wYjI0K1BIQnNZWFJtYjNKdFBreHBiblY0UEM5d2JHRjBabTl5YlQ0OFkzQjFQbmcyTkR3dlkzQjFQanhxWVhaaGMyTnlhWEIwUG01dlBDOXFZWFpoYzJOeWFYQjBQanhoWTNScGRtVjRQbTV2UEM5aFkzUnBkbVY0UGp4d2JIVm5hVzQrYm04OEwzQnNkV2RwYmo0OGJHRnVaR2x1WjNWeWFUNHZQQzlzWVc1a2FXNW5kWEpwUGp4c2IyTnJaV1J0YjJSbFBtNXZQQzlzYjJOclpXUnRiMlJsUGp4b2IzTjBibUZ0WlQ1a1IxWjZaRUU5UFR3dmFHOXpkRzVoYldVK1BHRndjRjlwWkQ0OEwyRndjRjlwWkQ0OEwyRm5aVzUwWDJsdVptOCsmYWdlbnRfcmVzdWx0PSZ0b2tlbj0xJnNpZ25hdHVyZT00c1krcFFkM3pyUTVjMkZsNUJ3a0JnPT0=\"\n\tif s != expected {\n\t\tt.Errorf(\"Client data doesn't correspond to expected: %s\", s)\n\t}\n}\n\nfunc TestUnmarshal(t *testing.T) {\n\t// parse https://f5.com/pre/config.php\n\tb := []byte(`<PROFILE VERSION=\"2.0\"><SERVERS><SITEM><ADDRESS>https://f5-1.com</ADDRESS><ALIAS>One</ALIAS></SITEM><SITEM><ADDRESS>https://f5-2.com</ADDRESS><ALIAS>Two</ALIAS></SITEM></SERVERS><SESSION LIMITED=\"YES\"><SAVEONEXIT>YES</SAVEONEXIT><SAVEPASSWORDS>NO</SAVEPASSWORDS><REUSEWINLOGONCREDS>NO</REUSEWINLOGONCREDS><REUSEWINLOGONSESSION>NO</REUSEWINLOGONSESSION><PASSWORD_POLICY><MODE>DISK</MODE><TIMEOUT>240</TIMEOUT></PASSWORD_POLICY><UPDATE><MODE>YES</MODE></UPDATE></SESSION><LOCATIONS><CORPORATE><DNSSUFFIX>corp.int</DNSSUFFIX><DNSSUFFIX>corp</DNSSUFFIX></CORPORATE></LOCATIONS></PROFILE>`)\n\tvar s config.PreConfigProfile\n\tif err := xml.Unmarshal(b, &s); err != nil {\n\t\tt.Errorf(\"failed to unmarshal a response: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "pkg/client/logger.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// Logger is an interface representing the Logger struct\ntype Logger interface {\n\tRequestPrintf(format string, args ...interface{})\n\tResponsePrintf(format string, args ...interface{})\n}\n\ntype logger struct {\n\tRequestID string\n}\n\nfunc (lg logger) RequestPrintf(format string, args ...interface{}) {\n\tfor _, v := range strings.Split(fmt.Sprintf(format, args...), \"\\n\") {\n\t\tlog.Printf(\"-> %s\", v)\n\t}\n}\n\nfunc (lg logger) ResponsePrintf(format string, args ...interface{}) {\n\tfor _, v := range strings.Split(fmt.Sprintf(format, args...), \"\\n\") {\n\t\tlog.Printf(\"<- %s\", v)\n\t}\n}\n\n// noopLogger is a default noop logger satisfies the Logger interface\ntype noopLogger struct{}\n\n// Printf is a default noop method\nfunc (noopLogger) RequestPrintf(format string, args ...interface{}) {}\n\n// Printf is a default noop method\nfunc (noopLogger) ResponsePrintf(format string, args ...interface{}) {}\n\n// RoundTripper satisfies the http.RoundTripper interface and is used to\n// customize the default http client RoundTripper\ntype RoundTripper struct {\n\t// Default http.RoundTripper\n\tRt http.RoundTripper\n\t// If Logger is not nil, then RoundTrip method will debug the JSON\n\t// requests and responses\n\tLogger Logger\n}\n\n// formatHeaders converts standard http.Header type to a string with separated headers.\nfunc (rt *RoundTripper) formatHeaders(headers http.Header, separator string) string {\n\tresult := make([]string, len(headers))\n\n\ti := 0\n\tfor header, data := range headers {\n\t\tresult[i] = fmt.Sprintf(\"%s: %s\", header, strings.Join(data, \" \"))\n\t\ti++\n\t}\n\n\treturn strings.Join(result, separator)\n}\n\n// RoundTrip performs a round-trip HTTP request and logs relevant information about it.\nfunc (rt *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {\n\tdefer func() {\n\t\tif request.Body != nil {\n\t\t\trequest.Body.Close()\n\t\t}\n\t}()\n\n\tvar err error\n\n\tif rt.Logger != nil {\n\t\trt.log().RequestPrintf(\"URL: %s %s\", request.Method, request.URL)\n\t\trt.log().RequestPrintf(\"Headers:\\n%s\", rt.formatHeaders(request.Header, \"\\n\"))\n\n\t\tif request.Body != nil {\n\t\t\trequest.Body, err = rt.logRequest(request.Body, request.Header.Get(\"Content-Type\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// this is concurrency safe\n\tort := rt.Rt\n\tif ort == nil {\n\t\treturn nil, fmt.Errorf(\"rt RoundTripper is nil, aborting\")\n\t}\n\tresponse, err := ort.RoundTrip(request)\n\n\tif response == nil {\n\t\tif rt.Logger != nil {\n\t\t\trt.log().ResponsePrintf(\"Connection error, retries exhausted. Aborting\")\n\t\t}\n\t\terr = fmt.Errorf(\"connection error, retries exhausted. Aborting. Last error was: %s\", err)\n\t\treturn nil, err\n\t}\n\n\tif rt.Logger != nil {\n\t\trt.log().ResponsePrintf(\"Code: %d\", response.StatusCode)\n\t\trt.log().ResponsePrintf(\"Headers:\\n%s\", rt.formatHeaders(response.Header, \"\\n\"))\n\n\t\tresponse.Body, err = rt.logResponse(response.Body, response.Header.Get(\"Content-Type\"))\n\t}\n\n\treturn response, err\n}\n\n// logRequest will log the HTTP Request details.\n// If the body is JSON, it will attempt to be pretty-formatted.\nfunc (rt *RoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) {\n\tvar bs bytes.Buffer\n\tdefer original.Close()\n\n\tif _, err := io.Copy(&bs, original); err != nil {\n\t\treturn nil, err\n\t}\n\n\trt.log().RequestPrintf(\"Body: %s\", bs.String())\n\n\treturn ioutil.NopCloser(bytes.NewReader(bs.Bytes())), nil\n}\n\n// logResponse will log the HTTP Response details.\n// If the body is JSON, it will attempt to be pretty-formatted.\nfunc (rt *RoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) {\n\tvar bs bytes.Buffer\n\tdefer original.Close()\n\n\tif _, err := io.Copy(&bs, original); err != nil {\n\t\treturn nil, err\n\t}\n\n\trt.log().ResponsePrintf(\"Body: %s\", bs.String())\n\n\treturn ioutil.NopCloser(bytes.NewReader(bs.Bytes())), nil\n}\n\nfunc (rt *RoundTripper) log() Logger {\n\t// this is concurrency safe\n\tl := rt.Logger\n\tif l == nil {\n\t\t// noop is used, when logger pointer has been set to nil\n\t\treturn &noopLogger{}\n\t}\n\treturn l\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\n\t\"github.com/kayrus/gof5/pkg/util\"\n\n\t\"gopkg.in/yaml.v2\"\n)\n\nconst (\n\tconfigDir  = \".gof5\"\n\tconfigName = \"config.yaml\"\n)\n\nvar (\n\tdefaultDNSListenAddr = net.IPv4(127, 0, 0, 0xf5).To4()\n\t// BSD systems don't support listeniing on 127.0.0.1+N\n\tdefaultBSDDNSListenAddr = net.IPv4(127, 0, 0, 1).To4()\n\tsupportedDrivers        = []string{\"wireguard\", \"pppd\"}\n)\n\nfunc ReadConfig(debug bool) (*Config, error) {\n\tvar err error\n\tvar usr *user.User\n\n\t// resolve sudo user ID\n\tif id, sudoUID := os.Geteuid(), os.Getenv(\"SUDO_UID\"); id == 0 && sudoUID != \"\" {\n\t\tusr, err = user.LookupId(sudoUID)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"failed to lookup user ID: %s\", err)\n\t\t\tif sudoUser := os.Getenv(\"SUDO_USER\"); sudoUser != \"\" {\n\t\t\t\tusr, err = user.Lookup(sudoUser)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to lookup user name: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// detect home directory\n\t\tusr, err = user.Current()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to detect home directory: %s\", err)\n\t\t}\n\t}\n\tconfigPath := filepath.Join(usr.HomeDir, configDir)\n\n\tvar uid, gid int\n\t// windows preserves the original user parameters, no need to detect uid/gid\n\tif runtime.GOOS != \"windows\" {\n\t\tuid, err = strconv.Atoi(usr.Uid)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to convert %q UID to integer: %s\", usr.Uid, err)\n\t\t}\n\t\tgid, err = strconv.Atoi(usr.Gid)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to convert %q GID to integer: %s\", usr.Uid, err)\n\t\t}\n\t}\n\n\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\tlog.Printf(\"%q directory doesn't exist, creating...\", configPath)\n\t\tif err := os.Mkdir(configPath, 0700); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create %q config directory: %s\", configPath, err)\n\t\t}\n\t\t// windows preserves the original user parameters, no need to chown\n\t\tif runtime.GOOS != \"windows\" {\n\t\t\tif err := os.Chown(configPath, uid, gid); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to set an owner for the %q config directory: %s\", configPath, err)\n\t\t\t}\n\t\t}\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get %q directory stat: %s\", configPath, err)\n\t}\n\n\tcfg := &Config{}\n\t// read config file\n\t// if config doesn't exist, use defaults\n\tif raw, err := ioutil.ReadFile(filepath.Join(configPath, configName)); err == nil {\n\t\tif err = yaml.Unmarshal(raw, cfg); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cannot parse %s file: %v\", configName, err)\n\t\t}\n\t} else {\n\t\tlog.Printf(\"Cannot read config file: %s\", err)\n\t}\n\n\t// set default driver\n\tif cfg.Driver == \"\" {\n\t\tcfg.Driver = \"wireguard\"\n\t}\n\n\tif cfg.Driver == \"wireguard\" {\n\t\tif err := checkWinTunDriver(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif cfg.Driver == \"pppd\" && runtime.GOOS == \"windows\" {\n\t\treturn nil, fmt.Errorf(\"pppd driver is not supported in Windows\")\n\t}\n\n\tif !util.StrSliceContains(supportedDrivers, cfg.Driver) {\n\t\treturn nil, fmt.Errorf(\"%q driver is unsupported, supported drivers are: %q\", cfg.Driver, supportedDrivers)\n\t}\n\n\tif cfg.ListenDNS == nil {\n\t\tswitch runtime.GOOS {\n\t\tcase \"freebsd\",\n\t\t\t\"darwin\":\n\t\t\tcfg.ListenDNS = defaultBSDDNSListenAddr\n\t\tdefault:\n\t\t\tcfg.ListenDNS = defaultDNSListenAddr\n\t\t}\n\t}\n\n\tcfg.Path = configPath\n\tcfg.Uid = uid\n\tcfg.Gid = gid\n\n\tcfg.Debug = debug\n\n\treturn cfg, nil\n}\n"
  },
  {
    "path": "pkg/config/types.go",
    "content": "package config\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/kayrus/gof5/pkg/util\"\n\n\t\"github.com/IBM/netaddr\"\n)\n\ntype Config struct {\n\tDebug             bool           `yaml:\"-\"`\n\tDriver            string         `yaml:\"driver\"`\n\tListenDNS         net.IP         `yaml:\"-\"`\n\tDNS               []string       `yaml:\"dns\"`\n\tOverrideDNS       []net.IP       `yaml:\"-\"`\n\tOverrideDNSSuffix []string       `yaml:\"overrideDNSSuffix\"`\n\tRoutes            *netaddr.IPSet `yaml:\"-\"`\n\tPPPdArgs          []string       `yaml:\"pppdArgs\"`\n\tInsecureTLS       bool           `yaml:\"insecureTLS\"`\n\tDTLS              bool           `yaml:\"dtls\"`\n\tIPv6              bool           `yaml:\"ipv6\"`\n\t// completely disable DNS servers handling\n\tDisableDNS bool `yaml:\"disableDNS\"`\n\t// rewrite /etc/resolv.conf instead of renaming\n\t// required in ChromeOS, where /etc/resolv.conf cannot be renamed\n\tRewriteResolv bool `yaml:\"rewriteResolv\"`\n\t// tls regeneration, tls.RenegotiateNever by default\n\tRenegotiation string `yaml:\"renegotiation\"`\n\t// list of detected local DNS servers\n\tDNSServers []net.IP `yaml:\"-\"`\n\t// config path\n\tPath string `yaml:\"-\"`\n\t// current user or sudo user UID\n\tUid int `yaml:\"-\"`\n\t// current user or sudo user GID\n\tGid int `yaml:\"-\"`\n\t// Config, returned by F5\n\tF5Config *Favorite `yaml:\"-\"`\n}\n\nfunc (r *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {\n\ttype tmp Config\n\tvar s struct {\n\t\ttmp\n\t\tListenDNS   *string  `yaml:\"listenDNS\"`\n\t\tRoutes      []string `yaml:\"routes\"`\n\t\tPPPdArgs    []string `yaml:\"pppdArgs\"`\n\t\tOverrideDNS []string `yaml:\"overrideDNS\"`\n\t}\n\n\tif err := unmarshal(&s.tmp); err != nil {\n\t\treturn err\n\t}\n\n\tif err := unmarshal(&s); err != nil {\n\t\treturn err\n\t}\n\n\t*r = Config(s.tmp)\n\n\tif s.ListenDNS != nil {\n\t\tr.ListenDNS = net.ParseIP(*s.ListenDNS)\n\t}\n\n\tr.Routes = new(netaddr.IPSet)\n\tif s.Routes != nil {\n\t\t// handle the case, when routes is an empty list\n\t\tparsedCIDRs, err := parseCIDRs(s.Routes, net.IPv4len)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr.Routes = subnetsToIPSet(parsedCIDRs)\n\t}\n\n\tif len(s.OverrideDNS) > 0 {\n\t\tr.OverrideDNS = processIPs(strings.Join(s.OverrideDNS, \" \"), net.IPv4len)\n\t}\n\n\t// default pppd arguments\n\tr.PPPdArgs = []string{\n\t\t\"logfd\", \"2\",\n\t\t\"noauth\",\n\t\t\"nodetach\",\n\t\t\"passive\",\n\t\t\"ipcp-accept-local\",\n\t\t\"ipcp-accept-remote\",\n\t\t\"notty\", // use default stdin/stdout\n\t\t\"nodefaultroute\",\n\t\t// nocompression\n\t\t\"novj\",\n\t\t\"novjccomp\",\n\t\t\"noaccomp\",\n\t\t\"noccp\",\n\t\t\"nopcomp\",\n\t\t\"nopredictor1\",\n\t\t\"nodeflate\", // Protocol-Reject for 'Compression Control Protocol' (0x80fd) received\n\t\t\"nobsdcomp\", // Protocol-Reject for 'Compression Control Protocol' (0x80fd) received\n\t}\n\tif len(s.PPPdArgs) > 0 {\n\t\t// extra pppd args\n\t\tr.PPPdArgs = append(r.PPPdArgs, s.PPPdArgs...)\n\t}\n\n\treturn nil\n}\n\ntype Favorite struct {\n\tObject Object `xml:\"object\"`\n}\n\ntype Bool bool\n\nfunc (b Bool) String() string {\n\tif b {\n\t\treturn \"yes\"\n\t}\n\treturn \"no\"\n}\n\nfunc (b Bool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {\n\treturn e.EncodeElement(b.String(), start)\n}\n\nfunc strToBool(s string) (Bool, error) {\n\tswitch v := strings.ToLower(s); v {\n\tcase \"yes\":\n\t\treturn true, nil\n\tcase \"no\", \"\":\n\t\treturn false, nil\n\t}\n\treturn false, fmt.Errorf(\"cannot parse boolean: %s\", s)\n}\n\n// TODO: unmarshal for bool\n\ntype Object struct {\n\tSessionID                      string         `xml:\"Session_ID\"`\n\tIPv4                           Bool           `xml:\"IPV4_0\"`\n\tIPv6                           Bool           `xml:\"IPV6_0\"`\n\tUrZ                            string         `xml:\"ur_Z\"`\n\tHDLCFraming                    Bool           `xml:\"-\"`\n\tHost                           string         `xml:\"host0\"`\n\tPort                           string         `xml:\"port0\"`\n\tTunnelHost                     string         `xml:\"tunnel_host0\"`\n\tTunnelPort                     string         `xml:\"tunnel_port0\"`\n\tAdd2Hosts                      string         `xml:\"Add2Hosts0\"`\n\tDNSRegisterConnection          int            `xml:\"DNSRegisterConnection0\"`\n\tDNSUseDNSSuffixForRegistration int            `xml:\"DNSUseDNSSuffixForRegistration0\"`\n\tSplitTunneling                 int            `xml:\"SplitTunneling0\"`\n\tDNSSPlit                       string         `xml:\"DNS_SPLIT0\"`\n\tTunnelDTLS                     bool           `xml:\"tunnel_dtls\"`\n\tTunnelPortDTLS                 string         `xml:\"tunnel_port_dtls\"`\n\tAllowLocalSubnetAccess         bool           `xml:\"AllowLocalSubnetAccess0\"`\n\tAllowLocalDNSServersAccess     bool           `xml:\"AllowLocalDNSServersAccess0\"`\n\tAllowLocalDHCPAccess           bool           `xml:\"AllowLocalDHCPAccess0\"`\n\tDNS                            []net.IP       `xml:\"-\"`\n\tDNS6                           []net.IP       `xml:\"-\"`\n\tExcludeSubnets                 []*net.IPNet   `xml:\"-\"`\n\tRoutes                         *netaddr.IPSet `xml:\"-\"`\n\tExcludeSubnets6                []*net.IPNet   `xml:\"-\"`\n\tRoutes6                        *netaddr.IPSet `xml:\"-\"`\n\tTrafficControl                 TrafficControl `xml:\"-\"`\n\tDNSSuffix                      []string       `xml:\"-\"`\n}\n\ntype TrafficControl struct {\n\tFlow []Flow `xml:\"flow\"`\n}\n\ntype Flow struct {\n\tName    string `xml:\"name,attr\"`\n\tRate    string `xml:\"rate,attr\"`\n\tCeiling string `xml:\"ceiling,attr\"`\n\tMode    string `xml:\"mode,attr\"`\n\tBurst   string `xml:\"burst,attr\"`\n\tType    string `xml:\"type,attr\"`\n\tVia     string `xml:\"via,attr\"`\n\tFilter  Filter `xml:\"filter\"`\n}\n\ntype Filter struct {\n\tProto   string `xml:\"proto,attr\"`\n\tSrc     string `xml:\"src,attr\"`\n\tSrcMask string `xml:\"src_mask,attr\"`\n\tSrcPort string `xml:\"src_port,attr\"`\n\tDst     string `xml:\"dst,attr\"`\n\tDstMask string `xml:\"dst_mask,attr\"`\n\tDstPort string `xml:\"dst_port,attr\"`\n}\n\nfunc (o *Object) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {\n\ttype tmp Object\n\tvar s struct {\n\t\ttmp\n\t\tDNS             string `xml:\"DNS0\"`\n\t\tDNS6            string `xml:\"DNS6_0\"`\n\t\tExcludeSubnets  string `xml:\"ExcludeSubnets0\"`\n\t\tExcludeSubnets6 string `xml:\"ExcludeSubnets6_0\"`\n\t\tTrafficControl  string `xml:\"TrafficControl0\"`\n\t\tHDLCFraming     string `xml:\"hdlc_framing\"`\n\t\tDNSSuffix       string `xml:\"DNSSuffix0\"`\n\t}\n\n\terr := d.DecodeElement(&s, &start)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*o = Object(s.tmp)\n\n\tif v, err := url.QueryUnescape(s.TrafficControl); err != nil {\n\t\treturn fmt.Errorf(\"failed to unescape %q: %s\", s.TrafficControl, err)\n\t} else if v := strings.TrimSpace(v); v != \"\" {\n\t\tif err = xml.Unmarshal([]byte(v), &o.TrafficControl); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\to.DNS = processIPs(s.DNS, net.IPv4len)\n\to.DNS6 = processIPs(s.DNS6, net.IPv6len)\n\to.ExcludeSubnets = processCIDRs(s.ExcludeSubnets, net.IPv4len)\n\to.ExcludeSubnets6 = processCIDRs(s.ExcludeSubnets6, net.IPv6len)\n\n\t// TODO: support IPv6 routes\n\to.Routes = inverseCIDRs4(o.ExcludeSubnets)\n\n\to.HDLCFraming, err = strToBool(s.HDLCFraming)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif v := strings.TrimSpace(s.DNSSuffix); v != \"\" {\n\t\to.DNSSuffix = strings.Split(v, \",\")\n\t}\n\n\treturn nil\n}\n\ntype Session struct {\n\tToken         string `xml:\"token\"`\n\tVersion       string `xml:\"version\"`\n\tRedirectURL   string `xml:\"redirect_url\"`\n\tMaxClientData string `xml:\"max_client_data\"`\n}\n\n// Profiles list\ntype Profiles struct {\n\tType      string         `xml:\"type,attr\"`\n\tLimited   string         `xml:\"limited,attr\"`\n\tFavorites []FavoriteItem `xml:\"favorite\"`\n}\n\ntype FavoriteItem struct {\n\tID      string `xml:\"id,attr\"`\n\tCaption string `xml:\"caption\"`\n\tName    string `xml:\"name\"`\n\tParams  string `xml:\"params\"`\n}\n\ntype Hostname string\n\nfunc (h Hostname) MarshalXML(e *xml.Encoder, start xml.StartElement) error {\n\treturn e.EncodeElement(base64.StdEncoding.EncodeToString([]byte(h)), start)\n}\n\nfunc processIPs(ips string, length int) []net.IP {\n\tif v := strings.FieldsFunc(strings.TrimSpace(ips), util.SplitFunc); len(v) > 0 {\n\t\tvar t []net.IP\n\t\tfor _, v := range v {\n\t\t\tv := net.ParseIP(v)\n\t\t\tif length == net.IPv4len {\n\t\t\t\tif v.To4() != nil {\n\t\t\t\t\tt = append(t, v)\n\t\t\t\t}\n\t\t\t} else if length == net.IPv6len {\n\t\t\t\tt = append(t, v.To16())\n\t\t\t}\n\t\t}\n\t\treturn t\n\t}\n\treturn nil\n}\n\nfunc parseCIDRs(cidrs []string, length int) ([]*net.IPNet, error) {\n\tt := make([]*net.IPNet, len(cidrs))\n\tfor i, v := range cidrs {\n\t\tvar cidr *net.IPNet\n\t\tvar err error\n\n\t\tif ip := net.ParseIP(v); ip != nil {\n\t\t\tcidr = &net.IPNet{\n\t\t\t\tIP:   ip,\n\t\t\t\tMask: net.CIDRMask(32, 32),\n\t\t\t}\n\t\t} else {\n\t\t\t// parse 1.2.3.4/12 format\n\t\t\t_, cidr, err = net.ParseCIDR(v)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse %q cidr: %v\", v, err)\n\t\t\t}\n\t\t}\n\t\tif length == net.IPv4len {\n\t\t\tt[i] = &net.IPNet{\n\t\t\t\tIP:   cidr.IP.To4(),\n\t\t\t\tMask: cidr.Mask,\n\t\t\t}\n\t\t} else if length == net.IPv6len {\n\t\t\tt[i] = &net.IPNet{\n\t\t\t\tIP:   cidr.IP.To16(),\n\t\t\t\tMask: cidr.Mask,\n\t\t\t}\n\t\t}\n\t}\n\treturn t, nil\n}\n\nfunc processCIDRs(cidrs string, length int) []*net.IPNet {\n\tif v := strings.FieldsFunc(strings.TrimSpace(cidrs), util.SplitFunc); len(v) > 0 {\n\t\tvar t []*net.IPNet\n\t\tfor _, v := range v {\n\t\t\t// parse 1.2.3.4/255.255.255.0 format\n\t\t\tif v := strings.Split(v, \"/\"); len(v) == 2 {\n\t\t\t\tip := net.ParseIP(v[0])\n\t\t\t\tmask := net.ParseIP(v[1])\n\t\t\t\tif ip == nil || mask == nil {\n\t\t\t\t\tlog.Printf(\"Cannot parse %q CIDR\", v)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif length == net.IPv4len {\n\t\t\t\t\tt = append(t, &net.IPNet{\n\t\t\t\t\t\tIP:   ip.To4(),\n\t\t\t\t\t\tMask: net.IPMask(mask.To4()),\n\t\t\t\t\t})\n\t\t\t\t} else if length == net.IPv6len {\n\t\t\t\t\tt = append(t, &net.IPNet{\n\t\t\t\t\t\tIP:   ip.To16(),\n\t\t\t\t\t\tMask: net.IPMask(mask.To16()),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Printf(\"Cannot parse %q CIDR\", v)\n\t\t}\n\t\treturn t\n\t}\n\treturn nil\n}\n\nfunc subnetsToIPSet(subnets []*net.IPNet) *netaddr.IPSet {\n\t// initialize an empty IPSet\n\tipSet4 := &netaddr.IPSet{}\n\n\tfor _, v := range subnets {\n\t\tipSet4.InsertNet(v)\n\t}\n\n\t// get a routes list\n\treturn ipSet4\n}\n\nfunc inverseCIDRs4(exclude []*net.IPNet) *netaddr.IPSet {\n\t// initialize an empty IPSet\n\tipSet4 := &netaddr.IPSet{}\n\n\tall := &net.IPNet{\n\t\tIP:   net.IPv4zero.To4(),\n\t\tMask: net.CIDRMask(0, 32),\n\t}\n\tipSet4.InsertNet(all)\n\n\t// remove reserved addresses (rfc8190)\n\tsoft := &net.IPNet{\n\t\tIP:   net.IPv4zero.To4(),\n\t\tMask: net.CIDRMask(8, 32),\n\t}\n\tipSet4.RemoveNet(soft)\n\n\tlocal := &net.IPNet{\n\t\tIP:   net.IPv4(127, 0, 0, 0).To4(),\n\t\tMask: net.CIDRMask(8, 32),\n\t}\n\tipSet4.RemoveNet(local)\n\n\tunicast := &net.IPNet{\n\t\tIP:   net.IPv4(169, 254, 0, 0).To4(),\n\t\tMask: net.CIDRMask(16, 32),\n\t}\n\tipSet4.RemoveNet(unicast)\n\n\tmulticast := &net.IPNet{\n\t\tIP:   net.IPv4(224, 0, 0, 0).To4(),\n\t\tMask: net.CIDRMask(4, 32),\n\t}\n\tipSet4.RemoveNet(multicast)\n\n\tfor _, v := range exclude {\n\t\tipSet4.RemoveNet(v)\n\t}\n\n\t// get a routes list\n\treturn ipSet4\n}\n\ntype AgentInfo struct {\n\tXMLName              xml.Name `xml:\"agent_info\"`\n\tType                 string   `xml:\"type\"`\n\tVersion              string   `xml:\"version\"`\n\tPlatform             string   `xml:\"platform\"`\n\tCPU                  string   `xml:\"cpu\"`\n\tJavaScript           Bool     `xml:\"javascript\"`\n\tActiveX              Bool     `xml:\"activex\"`\n\tPlugin               Bool     `xml:\"plugin\"`\n\tLandingURI           string   `xml:\"landinguri\"`\n\tModel                string   `xml:\"model,omitempty\"`\n\tPlatformVersion      string   `xml:\"platform_version,omitempty\"`\n\tMACAddress           string   `xml:\"mac_address,omitempty\"`\n\tUniqueID             string   `xml:\"unique_id,omitempty\"`\n\tSerialNumber         string   `xml:\"serial_number,omitempty\"`\n\tAppID                string   `xml:\"app_id,omitempty\"`\n\tAppVersion           string   `xml:\"app_version,omitempty\"`\n\tJailBreak            *Bool    `xml:\"jailbreak,omitempty\"`\n\tVPNScope             string   `xml:\"vpn_scope,omitempty\"`\n\tVPNStartType         string   `xml:\"vpn_start_type,omitempty\"`\n\tLockedMode           Bool     `xml:\"lockedmode\"`\n\tVPNTunnelType        string   `xml:\"vpn_tunnel_type,omitempty\"`\n\tHostname             Hostname `xml:\"hostname\"`\n\tBiometricFingerprint *Bool    `xml:\"biometric_fingerprint,omitempty\"`\n\tDevicePasscodeSet    *Bool    `xml:\"device_passcode_set,omitempty\"`\n}\n\ntype ClientData struct {\n\tXMLName       xml.Name `xml:\"data\"`\n\tToken         string   `xml:\"token\"`\n\tVersion       string   `xml:\"version\"`\n\tRedirectURL   string   `xml:\"redirect_url\"`\n\tMaxClientData int      `xml:\"max_client_data\"`\n}\n\ntype PreConfigProfile struct {\n\tXMLName   xml.Name         `xml:\"PROFILE\"`\n\tVersion   string           `xml:\"VERSION,attr\"`\n\tServers   []Server         `xml:\"SERVERS>SITEM\"`\n\tSession   preConfigSession `xml:\"SESSION\"`\n\tDNSSuffix []string         `xml:\"LOCATIONS>CORPORATE>DNSSUFFIX\"`\n}\n\ntype Server struct {\n\tAddress string `xml:\"ADDRESS\"`\n\tAlias   string `xml:\"ALIAS\"`\n}\n\ntype preConfigSession struct {\n\tLimited              Bool           `xml:\"-\"`\n\tSaveOnExit           Bool           `xml:\"-\"`\n\tSavePasswords        Bool           `xml:\"-\"`\n\tReuseWinlogonCreds   Bool           `xml:\"-\"`\n\tReuseWinlogonSession Bool           `xml:\"-\"`\n\tPasswordPolicy       PasswordPolicy `xml:\"PASSWORD_POLICY\"`\n\tUpdate               Update         `xml:\"UPDATE\"`\n}\n\nfunc (o *preConfigSession) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {\n\ttype tmp preConfigSession\n\tvar s struct {\n\t\ttmp\n\t\tLimited              string `xml:\"LIMITED,attr\"`\n\t\tSaveOnExit           string `xml:\"SAVEONEXIT\"`\n\t\tSavePasswords        string `xml:\"SAVEPASSWORDS\"`\n\t\tReuseWinlogonCreds   string `xml:\"REUSEWINLOGONCREDS\"`\n\t\tReuseWinlogonSession string `xml:\"REUSEWINLOGONSESSION\"`\n\t}\n\n\terr := d.DecodeElement(&s, &start)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*o = preConfigSession(s.tmp)\n\n\to.Limited, err = strToBool(s.Limited)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to.SaveOnExit, err = strToBool(s.SaveOnExit)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to.SavePasswords, err = strToBool(s.SavePasswords)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to.ReuseWinlogonCreds, err = strToBool(s.ReuseWinlogonCreds)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to.ReuseWinlogonSession, err = strToBool(s.ReuseWinlogonSession)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype PasswordPolicy struct {\n\tMode    string `xml:\"MODE\"`\n\tTimeout int    `xml:\"TIMEOUT\"`\n}\n\ntype Update struct {\n\tMode Bool `xml:\"-\"`\n}\n\nfunc (o *Update) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {\n\ttype tmp Update\n\tvar s struct {\n\t\ttmp\n\t\tMode string `xml:\"MODE\"`\n\t}\n\n\terr := d.DecodeElement(&s, &start)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*o = Update(s.tmp)\n\n\to.Mode, err = strToBool(s.Mode)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/config/wintun_other.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage config\n\nfunc checkWinTunDriver() error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/config/wintun_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nconst (\n\twinTun     = \"wintun.dll\"\n\twinTunSite = \"https://www.wintun.net/\"\n)\n\nfunc checkWinTunDriver() error {\n\terr := windows.NewLazyDLL(winTun).Load()\n\tif err != nil {\n\t\tdir, err := filepath.Abs(filepath.Dir(os.Args[0]))\n\t\tif err != nil {\n\t\t\tdir = \"gof5\"\n\t\t}\n\t\treturn fmt.Errorf(\"the %s was not found, you can download it from %s and place it into the %q directory\", winTun, winTunSite, dir)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cookie/cookie.go",
    "content": "package cookie\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n\n\t\"gopkg.in/yaml.v2\"\n)\n\nconst cookiesName = \"cookies.yaml\"\n\nfunc parseCookies(configPath string) map[string][]string {\n\tcookies := make(map[string][]string)\n\n\tcookiesPath := filepath.Join(configPath, cookiesName)\n\tv, err := ioutil.ReadFile(cookiesPath)\n\tif err != nil {\n\t\t// skip \"no such file or directory\" error on the first startup\n\t\tif e, ok := err.(*os.PathError); !ok || e.Unwrap() != syscall.ENOENT {\n\t\t\tlog.Printf(\"Cannot read cookies file: %v\", err)\n\t\t}\n\t\treturn cookies\n\t}\n\n\tif err = yaml.Unmarshal(v, &cookies); err != nil {\n\t\tlog.Printf(\"Cannot parse cookies: %v\", err)\n\t}\n\n\treturn cookies\n}\n\nfunc ReadCookies(c *http.Client, u *url.URL, cfg *config.Config, sessionID string) {\n\tv := parseCookies(cfg.Path)\n\tif v, ok := v[u.Host]; ok {\n\t\tvar cookies []*http.Cookie\n\t\tfor _, c := range v {\n\t\t\tif v := strings.Split(c, \"=\"); len(v) == 2 {\n\t\t\t\tcookies = append(cookies, &http.Cookie{Name: v[0], Value: v[1]})\n\t\t\t}\n\t\t}\n\t\tc.Jar.SetCookies(u, cookies)\n\t}\n\n\tif sessionID != \"\" {\n\t\tlog.Printf(\"Overriding session ID from a CLI argument\")\n\t\t// override session ID from CLI parameter\n\t\tcookies := []*http.Cookie{\n\t\t\t{Name: \"MRHSession\", Value: sessionID},\n\t\t}\n\t\tc.Jar.SetCookies(u, cookies)\n\t}\n}\n\nfunc SaveCookies(c *http.Client, u *url.URL, cfg *config.Config) error {\n\traw := parseCookies(cfg.Path)\n\t// empty current cookies list\n\traw[u.Host] = nil\n\t// write down new cookies\n\tfor _, c := range c.Jar.Cookies(u) {\n\t\traw[u.Host] = append(raw[u.Host], c.String())\n\t}\n\n\tcookies, err := yaml.Marshal(&raw)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot marshal cookies: %v\", err)\n\t}\n\n\tcookiesPath := filepath.Join(cfg.Path, cookiesName)\n\tif err = ioutil.WriteFile(cookiesPath, cookies, 0600); err != nil {\n\t\treturn fmt.Errorf(\"failed to save cookies: %s\", err)\n\t}\n\n\tif runtime.GOOS != \"windows\" {\n\t\tif err = os.Chown(cookiesPath, cfg.Uid, cfg.Gid); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set an owner for cookies file: %s\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/dns/dns.go",
    "content": "package dns\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n\n\t\"github.com/miekg/dns\"\n)\n\nfunc Start(cfg *config.Config, errChan chan error, tunDown chan struct{}) {\n\tdnsUDPHandler := func(w dns.ResponseWriter, m *dns.Msg) {\n\t\tdnsHandler(w, m, cfg, \"udp\")\n\t}\n\n\tdnsTCPHandler := func(w dns.ResponseWriter, m *dns.Msg) {\n\t\tdnsHandler(w, m, cfg, \"tcp\")\n\t}\n\n\tlisten := net.JoinHostPort(cfg.ListenDNS.String(), \"53\")\n\tsrvUDP := &dns.Server{\n\t\tAddr:    listen,\n\t\tNet:     \"udp\",\n\t\tHandler: dns.HandlerFunc(dnsUDPHandler),\n\t}\n\tsrvTCP := &dns.Server{\n\t\tAddr:    listen,\n\t\tNet:     \"tcp\",\n\t\tHandler: dns.HandlerFunc(dnsTCPHandler),\n\t}\n\n\tgo func() {\n\t\tif err := srvUDP.ListenAndServe(); err != nil {\n\t\t\terrChan <- fmt.Errorf(\"failed to set udp listener: %v\", err)\n\t\t\treturn\n\t\t}\n\t}()\n\tgo func() {\n\t\tif err := srvTCP.ListenAndServe(); err != nil {\n\t\t\terrChan <- fmt.Errorf(\"failed to set tcp listener: %v\", err)\n\t\t\treturn\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t<-tunDown\n\t\tlog.Printf(\"Shutting down DNS proxy\")\n\t\tsrvUDP.Shutdown()\n\t\tsrvTCP.Shutdown()\n\t}()\n}\n\nfunc dnsHandler(w dns.ResponseWriter, m *dns.Msg, cfg *config.Config, proto string) {\n\tc := new(dns.Client)\n\tfor _, suffix := range cfg.DNS {\n\t\tif strings.HasSuffix(m.Question[0].Name, suffix) {\n\t\t\tif cfg.Debug {\n\t\t\t\tlog.Printf(\"Resolving %q using VPN DNS\", m.Question[0].Name)\n\t\t\t}\n\t\t\tfor _, s := range cfg.F5Config.Object.DNS {\n\t\t\t\tif err := handleCustom(w, m, c, s); err == nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor _, s := range cfg.DNSServers {\n\t\tif err := handleCustom(w, m, c, s); err == nil {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc handleCustom(w dns.ResponseWriter, o *dns.Msg, c *dns.Client, ip net.IP) error {\n\tm := new(dns.Msg)\n\to.CopyTo(m)\n\tr, _, err := c.Exchange(m, net.JoinHostPort(ip.String(), \"53\"))\n\tif r == nil || err != nil {\n\t\treturn fmt.Errorf(\"failed to resolve %q\", m.Question[0].Name)\n\t}\n\tw.WriteMsg(r)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/link/cmd_nix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage link\n\nimport (\n\t\"log\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"syscall\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n)\n\nfunc Cmd(cfg *config.Config) *exec.Cmd {\n\tvar cmd *exec.Cmd\n\tif cfg.Driver == \"pppd\" {\n\t\t// VPN\n\t\tif cfg.IPv6 && bool(cfg.F5Config.Object.IPv6) {\n\t\t\tcfg.PPPdArgs = append(cfg.PPPdArgs,\n\t\t\t\t\"ipv6cp-accept-local\",\n\t\t\t\t\"ipv6cp-accept-remote\",\n\t\t\t\t\"+ipv6\",\n\t\t\t)\n\t\t} else {\n\t\t\tcfg.PPPdArgs = append(cfg.PPPdArgs,\n\t\t\t\t// TODO: clarify why it doesn't work\n\t\t\t\t\"noipv6\", // Unsupported protocol 'IPv6 Control Protocol' (0x8057) received\n\t\t\t)\n\t\t}\n\t\tif cfg.Debug {\n\t\t\tcfg.PPPdArgs = append(cfg.PPPdArgs,\n\t\t\t\t\"debug\",\n\t\t\t\t\"kdebug\", \"1\",\n\t\t\t)\n\t\t\tlog.Printf(\"pppd args: %q\", cfg.PPPdArgs)\n\t\t}\n\n\t\tswitch runtime.GOOS {\n\t\tdefault:\n\t\t\tcmd = exec.Command(\"pppd\", cfg.PPPdArgs...)\n\t\tcase \"freebsd\":\n\t\t\tcmd = exec.Command(\"ppp\", \"-direct\")\n\t\t}\n\n\t\t// don't forward parent process signals to a child process\n\t\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\t\tSetpgid: true,\n\t\t\tPgid:    0,\n\t\t}\n\t\treturn cmd\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/link/cmd_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage link\n\nimport (\n\t\"os/exec\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n)\n\nfunc Cmd(_ *config.Config) *exec.Cmd {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/link/f5.go",
    "content": "package link\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\n\t\"golang.org/x/net/ipv4\"\n\t\"golang.org/x/net/ipv6\"\n)\n\nfunc readBuf(buf, sep []byte) []byte {\n\tn := bytes.Index(buf, sep)\n\tif n == 0 {\n\t\treturn buf[len(sep):]\n\t}\n\treturn nil\n}\n\nvar (\n\tppp       = []byte{0xff, 0x03}\n\tpppLCP    = []byte{0xc0, 0x21}\n\tpppIPCP   = []byte{0x80, 0x21}\n\tpppIPv6CP = []byte{0x80, 0x57}\n\t// LCP auth\n\tmtuRequest = []byte{0x00, 0x18}\n\t// Link-Discriminator\n\tterminate = []byte{0x00, 0x17}\n\t// No network protocols\n\tnoProtocols = []byte{0x00, 0x20}\n\t// Session-Timeout\n\ttimeout = []byte{0x00, 0x13}\n\t//\n\tmtuResponse = []byte{0x00, 0x12}\n\tprotoRej    = []byte{0x00, 0x2c}\n\tmtuHeader   = []byte{0x01, 0x04}\n\tmtuSize     = 2\n\tipv6type    = []byte{0x00, 0x0e}\n\tipv4type    = []byte{0x00, 0x0a}\n\tv4          = []byte{0x06}\n\tv6          = []byte{0x0a}\n\tpfc         = []byte{0x07, 0x02}\n\tacfc        = []byte{0x08, 0x02}\n\taccm        = []byte{0x02, 0x06, 0x00, 0x00, 0x00, 0x00}\n\tmagicHeader = []byte{0x05, 0x06}\n\tmagicSize   = 4\n\tipv4header  = []byte{0x21}\n\tipv6header  = []byte{0x57}\n\t//\n\tconfRequest = []byte{0x01}\n\tconfAck     = []byte{0x02}\n\tconfNack    = []byte{0x03}\n\tconfRej     = []byte{0x04}\n\tconfTermReq = []byte{0x05}\n\tprotoReject = []byte{0x08}\n\techoReq     = []byte{0x09}\n\techoRep     = []byte{0x0a}\n)\n\nfunc bytesToIPv4(bytes []byte) net.IP {\n\treturn net.IP(append(bytes[:0:0], bytes...))\n}\n\nfunc bytesToIPv6(bytes []byte) net.IP {\n\treturn net.IP(append([]byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, append(bytes[:0:0], bytes...)...))\n}\n\nfunc processPPP(l *vpnLink, buf []byte, dstBuf *bytes.Buffer) error {\n\t// process ipv4 traffic\n\tif v := readBuf(buf, ipv4header); v != nil {\n\t\tif l.debug {\n\t\t\tlog.Printf(\"Read parsed ipv4 %d bytes from http:\\n%s\", len(v), hex.Dump(v))\n\t\t\theader, _ := ipv4.ParseHeader(v)\n\t\t\tlog.Printf(\"ipv4 from http: %s\", header)\n\t\t}\n\n\t\twn, err := l.iface.Write(v)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"fatal write to tun: %s\", err)\n\t\t}\n\t\tif l.debug {\n\t\t\tlog.Printf(\"Sent %d bytes to tun\", wn)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// process ipv6 traffic\n\tif v := readBuf(buf, ipv6header); v != nil {\n\t\tif l.debug {\n\t\t\tlog.Printf(\"Read parsed ipv6 %d bytes from http:\\n%s\", len(v), hex.Dump(v))\n\t\t\theader, _ := ipv6.ParseHeader(v)\n\t\t\tlog.Printf(\"ipv6 from http: %s\", header)\n\t\t}\n\n\t\twn, err := l.iface.Write(v)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"fatal write to tun: %s\", err)\n\t\t}\n\t\tif l.debug {\n\t\t\tlog.Printf(\"Sent %d bytes to tun\", wn)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// TODO: support IPv4 only\n\tif v := readBuf(buf, pppIPCP); v != nil {\n\t\tif v := readBuf(v, confRequest); v != nil {\n\t\t\tid := v[0]\n\t\t\tif v := readBuf(v[1:], ipv4type); v != nil {\n\t\t\t\tid2 := v[0]\n\t\t\t\tif v := readBuf(v[1:], v4); v != nil {\n\t\t\t\t\tl.serverIPv4 = bytesToIPv4(v)\n\t\t\t\t\tlog.Printf(\"id: %d, id2: %d, Remote IPv4 requested: %s\", id, id2, l.serverIPv4)\n\n\t\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\tdoResp.Write(pppIPCP)\n\t\t\t\t\t//\n\t\t\t\t\tdoResp.Write(confAck)\n\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\tdoResp.Write(ipv4type)\n\t\t\t\t\tdoResp.WriteByte(id2)\n\t\t\t\t\tdoResp.Write(v4)\n\t\t\t\t\tdoResp.Write(v)\n\n\t\t\t\t\terr := toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tdoResp = &bytes.Buffer{}\n\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\tdoResp.Write(pppIPCP)\n\t\t\t\t\t//\n\t\t\t\t\tdoResp.Write(confRequest)\n\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\tdoResp.Write(ipv4type)\n\t\t\t\t\tdoResp.WriteByte(id2)\n\t\t\t\t\tdoResp.Write(v4)\n\t\t\t\t\tfor i := 0; i < 4; i++ {\n\t\t\t\t\t\tdoResp.WriteByte(0)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif v := readBuf(v, confAck); v != nil {\n\t\t\tid := v[0]\n\t\t\tif v := readBuf(v[1:], ipv4type); v != nil {\n\t\t\t\tid2 := v[0]\n\t\t\t\tif v := readBuf(v[1:], v4); v != nil {\n\t\t\t\t\tl.localIPv4 = bytesToIPv4(v)\n\t\t\t\t\tlog.Printf(\"id: %d, id2: %d, Local IPv4 acknowledged: %s\", id, id2, l.localIPv4)\n\n\t\t\t\t\t// connection established\n\t\t\t\t\tclose(l.pppUp)\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif v := readBuf(v, confNack); v != nil {\n\t\t\tid := v[0]\n\t\t\tif v := readBuf(v[1:], ipv4type); v != nil {\n\t\t\t\tid2 := v[0]\n\t\t\t\tif v := readBuf(v[1:], v4); v != nil {\n\t\t\t\t\tlog.Printf(\"id: %d, id2: %d, Local IPv4 not acknowledged: %s\", id, id2, bytesToIPv4(v))\n\n\t\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\tdoResp.Write(pppIPCP)\n\t\t\t\t\t//\n\t\t\t\t\tdoResp.Write(confRequest)\n\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\tdoResp.Write(ipv4type)\n\t\t\t\t\tdoResp.WriteByte(id2)\n\t\t\t\t\tdoResp.Write(v4)\n\t\t\t\t\tdoResp.Write(v)\n\n\t\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// pppIPv6CP\n\tif v := readBuf(buf, pppIPv6CP); v != nil {\n\t\tif v := readBuf(v, confRequest); v != nil {\n\t\t\tid := v[0]\n\t\t\tif v := readBuf(v[1:], ipv6type); v != nil {\n\t\t\t\tid2 := v[0]\n\t\t\t\tif v := readBuf(v[1:], v6); v != nil {\n\t\t\t\t\tl.serverIPv6 = bytesToIPv6(v)\n\t\t\t\t\tlog.Printf(\"id: %d, id2: %d, Remote IPv6 requested: %s\", id, id2, l.serverIPv6)\n\n\t\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\tdoResp.Write(pppIPv6CP)\n\t\t\t\t\t//\n\t\t\t\t\tdoResp.Write(confAck)\n\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\tdoResp.Write(ipv6type)\n\t\t\t\t\tdoResp.WriteByte(id2)\n\t\t\t\t\tdoResp.Write(v6)\n\t\t\t\t\tdoResp.Write(v)\n\n\t\t\t\t\terr := toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tdoResp = &bytes.Buffer{}\n\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\tdoResp.Write(pppIPv6CP)\n\t\t\t\t\t//\n\t\t\t\t\tdoResp.Write(confRequest)\n\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\tdoResp.Write(ipv6type)\n\t\t\t\t\tdoResp.WriteByte(id2)\n\t\t\t\t\tdoResp.Write(v6)\n\t\t\t\t\tfor i := 0; i < 8; i++ {\n\t\t\t\t\t\tdoResp.WriteByte(0)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif v := readBuf(v, confAck); v != nil {\n\t\t\tid := v[0]\n\t\t\tif v := readBuf(v[1:], ipv6type); v != nil {\n\t\t\t\tid2 := v[0]\n\t\t\t\tif v := readBuf(v[1:], v6); v != nil {\n\t\t\t\t\tl.localIPv6 = bytesToIPv6(v)\n\t\t\t\t\tlog.Printf(\"id: %d, id2: %d, Local IPv6 acknowledged: %s\", id, id2, l.localIPv6)\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif v := readBuf(v, confNack); v != nil {\n\t\t\tid := v[0]\n\t\t\tif v := readBuf(v[1:], ipv6type); v != nil {\n\t\t\t\tid2 := v[0]\n\t\t\t\tif v := readBuf(v[1:], v6); v != nil {\n\t\t\t\t\tlog.Printf(\"id: %d, id2: %d, Local IPv6 not acknowledged: %s\", id, id2, bytesToIPv6(v))\n\n\t\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\tdoResp.Write(pppIPv6CP)\n\t\t\t\t\t//\n\t\t\t\t\tdoResp.Write(confRequest)\n\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\tdoResp.Write(ipv6type)\n\t\t\t\t\tdoResp.WriteByte(id2)\n\t\t\t\t\tdoResp.Write(v6)\n\t\t\t\t\tdoResp.Write(v)\n\n\t\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// it is PPP header\n\tif v := readBuf(buf, ppp); v != nil {\n\t\t// it is pppLCP\n\t\tif v := readBuf(v, pppLCP); v != nil {\n\t\t\tif v := readBuf(v, confTermReq); v != nil {\n\t\t\t\tid := v[0]\n\t\t\t\tif v := readBuf(v[1:], terminate); v != nil {\n\t\t\t\t\treturn fmt.Errorf(\"id: %d, Link terminated with: %s\", id, v)\n\t\t\t\t}\n\t\t\t\tif v := readBuf(v[1:], timeout); v != nil {\n\t\t\t\t\treturn fmt.Errorf(\"id: %d, Link timed out with: %s\", id, v)\n\t\t\t\t}\n\t\t\t\tif v := readBuf(v[1:], noProtocols); v != nil {\n\t\t\t\t\treturn fmt.Errorf(\"id: %d, Link terminated with: %s\", id, v)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif v := readBuf(v, echoReq); v != nil {\n\t\t\t\tid := v[0]\n\t\t\t\tif l.debug {\n\t\t\t\t\tlog.Printf(\"id: %d, echo\", id)\n\t\t\t\t}\n\t\t\t\t// live pings\n\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\tdoResp.Write(ppp)\n\t\t\t\tdoResp.Write(pppLCP)\n\t\t\t\t//\n\t\t\t\tdoResp.Write(echoRep)\n\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\tdoResp.Write(v[1:])\n\n\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t}\n\t\t\tif v := readBuf(v, protoReject); v != nil {\n\t\t\t\tid := v[0]\n\t\t\t\tif v := readBuf(v[1:], protoRej); v != nil {\n\t\t\t\t\tlog.Printf(\"id: %d, Protocol reject:\\n%s\", id, hex.Dump(v))\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\t// it is pppLCP\n\t\t\tif v := readBuf(v, confRequest); v != nil {\n\t\t\t\tid := v[0]\n\t\t\t\t// configuration requested\n\t\t\t\tif v := readBuf(v[1:], mtuRequest); v != nil {\n\t\t\t\t\t// MTU request\n\t\t\t\t\tif v := readBuf(v, mtuHeader); v != nil {\n\t\t\t\t\t\t// set MTU\n\t\t\t\t\t\tt := v[:mtuSize]\n\t\t\t\t\t\tl.mtu = append(t[:0:0], t...)\n\t\t\t\t\t\tl.mtuInt = binary.BigEndian.Uint16(l.mtu)\n\t\t\t\t\t\tlog.Printf(\"MTU: %d\", l.mtuInt)\n\t\t\t\t\t\tif v := readBuf(v[mtuSize:], accm); v != nil {\n\t\t\t\t\t\t\tif v := readBuf(v, magicHeader); v != nil {\n\t\t\t\t\t\t\t\tmagic := v[:magicSize]\n\t\t\t\t\t\t\t\tlog.Printf(\"Magic: %x\", magic)\n\t\t\t\t\t\t\t\tlog.Printf(\"PFC: %x\", v[magicSize:magicSize+len(pfc)])\n\t\t\t\t\t\t\t\tlog.Printf(\"ACFC: %x\", v[magicSize+len(pfc):])\n\n\t\t\t\t\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\t\t\t\tdoResp.Write(pppLCP)\n\t\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t\tdoResp.Write(confRequest)\n\t\t\t\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\t\t\t\tdoResp.Write(ipv6type)\n\t\t\t\t\t\t\t\tdoResp.Write(accm)\n\t\t\t\t\t\t\t\tdoResp.Write(pfc)\n\t\t\t\t\t\t\t\tdoResp.Write(acfc)\n\n\t\t\t\t\t\t\t\terr := toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tdoResp = &bytes.Buffer{}\n\t\t\t\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\t\t\t\tdoResp.Write(pppLCP)\n\t\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t\tdoResp.Write(confRej)\n\t\t\t\t\t\t\t\t//doResp.Write(confRequest)\n\t\t\t\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\t\t\t\tdoResp.Write(ipv4type)\n\t\t\t\t\t\t\t\tdoResp.Write(magicHeader)\n\t\t\t\t\t\t\t\tdoResp.Write(magic)\n\n\t\t\t\t\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn fmt.Errorf(\"wrong magic header: %x\", v)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn fmt.Errorf(\"wrong ACCM: %x\", v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif v := readBuf(v[1:], mtuResponse); v != nil {\n\t\t\t\t\tif v := readBuf(v, mtuHeader); v != nil {\n\t\t\t\t\t\tif v := readBuf(v, l.mtu); v != nil {\n\t\t\t\t\t\t\tif v := readBuf(v, accm); v != nil {\n\t\t\t\t\t\t\t\tif v := readBuf(v, pfc); v != nil {\n\t\t\t\t\t\t\t\t\tif v := readBuf(v, acfc); v != nil {\n\t\t\t\t\t\t\t\t\t\tlog.Printf(\"id: %d, MTU accepted\", id)\n\n\t\t\t\t\t\t\t\t\t\tdoResp := &bytes.Buffer{}\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(ppp)\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(pppLCP)\n\t\t\t\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(confAck)\n\t\t\t\t\t\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(mtuResponse)\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(mtuHeader)\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(l.mtu)\n\t\t\t\t\t\t\t\t\t\tdoResp.WriteByte(id)\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(v4)\n\t\t\t\t\t\t\t\t\t\tfor i := 0; i < 4; i++ {\n\t\t\t\t\t\t\t\t\t\t\tdoResp.WriteByte(0)\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(pfc)\n\t\t\t\t\t\t\t\t\t\tdoResp.Write(acfc)\n\n\t\t\t\t\t\t\t\t\t\treturn toF5(l, doResp.Bytes(), dstBuf)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// do set\n\t\t\tif v := readBuf(v, confAck); v != nil {\n\t\t\t\t// required settings\n\t\t\t\tid := v[0]\n\t\t\t\tif v := readBuf(v[1:], ipv6type); v != nil {\n\t\t\t\t\tif v := readBuf(v, accm); v != nil {\n\t\t\t\t\t\tif v := readBuf(v, pfc); v != nil {\n\t\t\t\t\t\t\tif v := readBuf(v, acfc); v != nil {\n\t\t\t\t\t\t\t\tlog.Printf(\"id: %d, IPV6 accepted\", id)\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif v := readBuf(v, confNack); v != nil {\n\t\t\t\tid := v[0]\n\t\t\t\tif v := readBuf(v[1:], mtuRequest); v != nil {\n\t\t\t\t\tif v := readBuf(v, mtuHeader); v != nil {\n\t\t\t\t\t\tif v := readBuf(v, l.mtu); v != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"id: %d, MTU not acknowledged:\\n%s\", id, hex.Dump(v))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif v := readBuf(v[1:], ipv4type); v != nil {\n\t\t\t\t\tif v := readBuf(v, magicHeader); v != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"id: %d, IPv4 not acknowledged:\\n%s\", id, hex.Dump(v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"unknown PPP data:\\n%s\", hex.Dump(buf))\n}\n\nfunc fromF5(l *vpnLink, dstBuf *bytes.Buffer) error {\n\t// read the F5 packet header\n\tbuf := make([]byte, 2)\n\t_, err := io.ReadFull(l.HTTPConn, buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read F5 packet header: %s\", err)\n\t}\n\tif !(buf[0] == 0xf5 && buf[1] == 00) {\n\t\treturn fmt.Errorf(\"incorrect F5 header: %x\", buf)\n\t}\n\n\t// read the F5 packet size\n\tvar pkglen uint16\n\terr = binary.Read(l.HTTPConn, binary.BigEndian, &pkglen)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read F5 packet size: %s\", err)\n\t}\n\n\t// read the packet\n\tbuf = make([]byte, pkglen)\n\tn, err := io.ReadFull(l.HTTPConn, buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read F5 packet of the %d size: %s\", pkglen, err)\n\t}\n\tif n != int(pkglen) {\n\t\treturn fmt.Errorf(\"incorrect F5 packet size: %d, expected: %d\", n, pkglen)\n\t}\n\n\t// process the packet\n\treturn processPPP(l, buf, dstBuf)\n}\n\n// Decode F5 packet\n// http->tun\nfunc (l *vpnLink) HttpToTun() {\n\tdstBuf := &bytes.Buffer{}\n\tfor {\n\t\tselect {\n\t\tcase <-l.TunDown:\n\t\t\treturn\n\t\tdefault:\n\t\t\terr := fromF5(l, dstBuf)\n\t\t\tif err != nil {\n\t\t\t\tl.ErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc toF5(l *vpnLink, buf []byte, dst *bytes.Buffer) error {\n\t// TODO: move buffer initialization into tunToHTTP\n\t// probably a buffered pipe would be nicer\n\tlength := len(buf)\n\tif length == 0 {\n\t\treturn fmt.Errorf(\"cannot encapsulate zero packet\")\n\t}\n\n\tdefer dst.Reset()\n\n\t// TODO: check packet header length (ipv4.HeaderLen, ipv6.HeaderLen)\n\tswitch buf[0] >> 4 {\n\tcase ipv4.Version:\n\t\tlength += len(ipv4header)\n\tcase ipv6.Version:\n\t\tlength += len(ipv6header)\n\t}\n\n\t_, err := dst.Write([]byte{0xf5, 0x00})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write F5 header: %s\", err)\n\t}\n\terr = binary.Write(dst, binary.BigEndian, uint16(length))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write F5 header size: %s\", err)\n\t}\n\n\tswitch buf[0] >> 4 {\n\tcase ipv4.Version:\n\t\t_, err = dst.Write(ipv4header)\n\tcase ipv6.Version:\n\t\t_, err = dst.Write(ipv6header)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write IP header: %s\", err)\n\t}\n\n\tif l.debug {\n\t\tlog.Printf(\"Sending from pppd:\\n%s\", hex.Dump(buf))\n\t}\n\n\t_, err = dst.Write(buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"fatal write to http: %s\", err)\n\t}\n\twn, err := io.Copy(l.HTTPConn, dst)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"fatal write to http: %s\", err)\n\t}\n\tif l.debug {\n\t\tlog.Printf(\"Sent %d bytes to http\", wn)\n\t}\n\n\treturn nil\n}\n\n// Encode into F5 packet\n// tun->http\nfunc (l *vpnLink) TunToHTTP() {\n\tbuf := make([]byte, bufferSize)\n\tdstBuf := &bytes.Buffer{}\n\tfor {\n\t\tselect {\n\t\tcase <-l.TunDown:\n\t\t\treturn\n\t\tcase <-l.tunUp:\n\t\t\trn, err := l.iface.Read(buf)\n\t\t\tif err != nil {\n\t\t\t\tif err != io.EOF {\n\t\t\t\t\tl.ErrChan <- fmt.Errorf(\"fatal read tun: %s\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif l.debug {\n\t\t\t\tlog.Printf(\"Read %d bytes from tun:\\n%s\", rn, hex.Dump(buf[:rn]))\n\t\t\t\theader, _ := ipv4.ParseHeader(buf[:rn])\n\t\t\t\tlog.Printf(\"ipv4 from tun: %s\", header)\n\t\t\t}\n\n\t\t\terr = toF5(l, buf[:rn], dstBuf)\n\t\t\tif err != nil {\n\t\t\t\tl.ErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/link/link.go",
    "content": "package link\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/kayrus/gof5/pkg/config\"\n\t\"github.com/kayrus/gof5/pkg/dns\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/kayrus/tuncfg/resolv\"\n\t\"github.com/kayrus/tuncfg/route\"\n\t\"github.com/kayrus/tuncfg/tun\"\n\t\"github.com/pion/dtls/v2\"\n)\n\nconst (\n\t// TUN MTU should not be bigger than buffer size\n\tbufferSize   = 1500\n\tuserAgentVPN = \"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0; F5 Networks Client)\"\n)\n\nvar colorlog = log.New(color.Error, \"\", log.LstdFlags)\n\ntype vpnLink struct {\n\tsync.Mutex\n\tHTTPConn    io.ReadWriteCloser\n\tErrChan     chan error\n\tTunDown     chan struct{}\n\tPppdErrChan chan error\n\tiface       io.ReadWriteCloser\n\tname        string\n\t// pppUp is used to wait for the PPP handshake (wireguard only)\n\tpppUp chan struct{}\n\t// tunUp is used to wait for the TUN interface (wireguard and pppd)\n\ttunUp         chan struct{}\n\tserverIPs     []net.IP\n\tlocalIPv4     net.IP\n\tserverIPv4    net.IP\n\tlocalIPv6     net.IP\n\tserverIPv6    net.IP\n\tmtu           []byte\n\tmtuInt        uint16\n\tdebug         bool\n\trouteHandler  *route.Handler\n\tresolvHandler *resolv.Handler\n}\n\nfunc randomHostname(n int) []byte {\n\tvar letters = []byte(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n\n\trand.Seed(time.Now().UnixNano())\n\n\tb := make([]byte, n)\n\tfor i := range b {\n\t\tb[i] = letters[rand.Intn(len(letters))]\n\t}\n\treturn b\n}\n\n// init a TLS connection\nfunc InitConnection(server string, cfg *config.Config, tlsConfig *tls.Config) (*vpnLink, error) {\n\tgetURL := fmt.Sprintf(\"https://%s/myvpn?sess=%s&hostname=%s&hdlc_framing=%s&ipv4=%s&ipv6=%s&Z=%s\",\n\t\tserver,\n\t\tcfg.F5Config.Object.SessionID,\n\t\tbase64.StdEncoding.EncodeToString(randomHostname(8)),\n\t\tconfig.Bool(cfg.Driver == \"pppd\"),\n\t\tcfg.F5Config.Object.IPv4,\n\t\tconfig.Bool(cfg.IPv6 && bool(cfg.F5Config.Object.IPv6)),\n\t\tcfg.F5Config.Object.UrZ,\n\t)\n\n\tserverIPs, err := net.LookupIP(server)\n\tif err != nil || len(serverIPs) == 0 {\n\t\treturn nil, fmt.Errorf(\"failed to resolve %s: %s\", server, err)\n\t}\n\n\t// define link channels\n\tl := &vpnLink{\n\t\tErrChan:     make(chan error, 1),\n\t\tTunDown:     make(chan struct{}, 1),\n\t\tPppdErrChan: make(chan error, 1),\n\t\tserverIPs:   serverIPs,\n\t\tpppUp:       make(chan struct{}, 1),\n\t\ttunUp:       make(chan struct{}, 1),\n\t\tdebug:       cfg.Debug,\n\t}\n\n\tif cfg.DTLS && cfg.F5Config.Object.TunnelDTLS {\n\t\ts := fmt.Sprintf(\"%s:%s\", server, cfg.F5Config.Object.TunnelPortDTLS)\n\t\tlog.Printf(\"Connecting to %s using DTLS\", s)\n\t\taddr, err := net.ResolveUDPAddr(\"udp\", s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to resolve UDP address: %s\", err)\n\t\t}\n\t\tconf := &dtls.Config{\n\t\t\tRootCAs:            tlsConfig.RootCAs,\n\t\t\tCertificates:       tlsConfig.Certificates,\n\t\t\tInsecureSkipVerify: tlsConfig.InsecureSkipVerify,\n\t\t\tServerName:         server,\n\t\t}\n\t\tl.HTTPConn, err = dtls.Dial(\"udp\", addr, conf)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to dial %s:%s: %s\", server, cfg.F5Config.Object.TunnelPortDTLS, err)\n\t\t}\n\t} else {\n\t\tl.HTTPConn, err = tls.Dial(\"tcp\", fmt.Sprintf(\"%s:443\", server), tlsConfig)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to dial %s:443: %s\", server, err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequest(\"GET\", getURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create VPN session request: %s\", err)\n\t}\n\treq.Header.Set(\"User-Agent\", userAgentVPN)\n\terr = req.Write(l.HTTPConn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send VPN session request: %s\", err)\n\t}\n\n\tif l.debug {\n\t\tlog.Printf(\"URL: %s\", getURL)\n\t}\n\n\tresp, err := http.ReadResponse(bufio.NewReader(l.HTTPConn), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get initial VPN connection response: %s\", err)\n\t}\n\tresp.Body.Close()\n\n\tl.localIPv4 = net.ParseIP(resp.Header.Get(\"X-VPN-client-IP\"))\n\tl.serverIPv4 = net.ParseIP(resp.Header.Get(\"X-VPN-server-IP\"))\n\tl.localIPv6 = net.ParseIP(resp.Header.Get(\"X-VPN-client-IPv6\"))\n\tl.serverIPv6 = net.ParseIP(resp.Header.Get(\"X-VPN-server-IPv6\"))\n\n\tif l.debug {\n\t\tlog.Printf(\"Client IP: %s\", l.localIPv4)\n\t\tlog.Printf(\"Server IP: %s\", l.serverIPv4)\n\t\tif l.localIPv6 != nil {\n\t\t\tlog.Printf(\"Client IPv6: %s\", l.localIPv6)\n\t\t}\n\t\tif l.localIPv6 != nil {\n\t\t\tlog.Printf(\"Server IPv6: %s\", l.serverIPv6)\n\t\t}\n\t}\n\n\treturn l, nil\n}\n\nfunc (l *vpnLink) createTunDevice() error {\n\tif l.mtuInt+tun.Offset > bufferSize {\n\t\treturn fmt.Errorf(\"MTU exceeds the %d buffer limit\", bufferSize)\n\t}\n\n\tlog.Printf(\"Using wireguard module to create tunnel\")\n\tifname := \"\"\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tifname = \"utun\"\n\tcase \"windows\":\n\t\tifname = \"gof5\"\n\t}\n\n\tlocal := &net.IPNet{\n\t\tIP:   l.localIPv4,\n\t\tMask: net.CIDRMask(32, 32),\n\t}\n\tgw := &net.IPNet{\n\t\tIP:   l.serverIPv4,\n\t\tMask: net.CIDRMask(32, 32),\n\t}\n\ttunDev, err := tun.OpenTunDevice(local, gw, ifname, int(l.mtuInt))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create an interface: %s\", err)\n\t}\n\tl.name, err = tunDev.Name()\n\tif err != nil {\n\t\tif e := tunDev.Close(); e != nil {\n\t\t\tlog.Printf(\"error closing interface: %v\", e)\n\t\t}\n\t\treturn fmt.Errorf(\"failed to get an interface name: %s\", err)\n\t}\n\n\tlog.Printf(\"Created %s interface\", l.name)\n\tl.iface = &tun.Tunnel{NativeTun: tunDev}\n\n\t// can now process the traffic\n\tclose(l.tunUp)\n\n\treturn nil\n}\n\nfunc (l *vpnLink) configureDNS(cfg *config.Config) error {\n\tvar err error\n\t// this is used only in linux/freebsd to store /etc/resolv.conf backup\n\tresolv.AppName = \"gof5\"\n\n\tdnsSuffixes := cfg.F5Config.Object.DNSSuffix\n\tvar dnsServers []net.IP\n\tif len(cfg.DNS) == 0 {\n\t\t// route everything through VPN gatewy\n\t\tdnsServers = cfg.F5Config.Object.DNS\n\t} else {\n\t\t// route only configured suffixes via local DNS proxy\n\t\tdnsServers = []net.IP{cfg.ListenDNS}\n\t}\n\n\t// define DNS servers, provided by F5\n\tl.resolvHandler, err = resolv.New(l.name, dnsServers, dnsSuffixes, cfg.RewriteResolv)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif cfg.DisableDNS {\n\t\t// TODO: this is a hack to get real DNS servers, need to be fixed in \"tuncfg\"\n\t\tl.resolvHandler.IsResolve()\n\t\t// no further configuration is required\n\t\t// get current DNS setting and exit\n\t\treturn nil\n\t}\n\n\tif len(cfg.DNS) > 0 && !l.resolvHandler.IsResolve() {\n\t\t// combine local network search with VPN gateway search\n\t\tdnsSuffixes = l.resolvHandler.GetOriginalSuffixes()\n\t\texistingSuffixes := make(map[string]bool)\n\t\tfor _, existingSuffix := range dnsSuffixes {\n\t\t\texistingSuffixes[existingSuffix] = true\n\t\t}\n\n\t\tfor _, newSuffix := range cfg.F5Config.Object.DNSSuffix {\n\t\t\tif !existingSuffixes[newSuffix] {\n\t\t\t\tdnsSuffixes = append(dnsSuffixes, newSuffix)\n\t\t\t}\n\t\t}\n\t\tl.resolvHandler.SetSuffixes(dnsSuffixes)\n\t}\n\n\tif l.resolvHandler.IsResolve() {\n\t\t// resolve daemon will route necessary domains through VPN gatewy\n\t\tlog.Printf(\"Detected systemd-resolved\")\n\t\tl.resolvHandler.SetDNSServers(cfg.F5Config.Object.DNS)\n\t\tif len(cfg.DNS) > 0 {\n\t\t\tlog.Printf(\"Forwarding %q DNS requests to %q\", cfg.DNS, cfg.F5Config.Object.DNS)\n\t\t\tl.resolvHandler.SetDNSDomains(cfg.DNS)\n\t\t\tlog.Printf(\"Default DNS servers: %q\", l.resolvHandler.GetOriginalDNS())\n\t\t} else {\n\t\t\t// route all DNS queries via VPN\n\t\t\tlog.Printf(\"Forwarding all DNS requests to %q\", cfg.F5Config.Object.DNS)\n\t\t\tl.resolvHandler.SetDNSDomains([]string{\".\"})\n\t\t}\n\t}\n\n\t// set DNS and additionally detect original DNS servers, e.g. when NetworkManager is used\n\terr = l.resolvHandler.Set()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !l.resolvHandler.IsResolve() {\n\t\tif len(cfg.DNS) == 0 {\n\t\t\tlog.Printf(\"Forwarding all DNS requests to %q\", cfg.F5Config.Object.DNS)\n\t\t\treturn nil\n\t\t}\n\t\tcfg.DNSServers = l.resolvHandler.GetOriginalDNS()\n\t\tlog.Printf(\"Serving DNS proxy on %s:53\", cfg.ListenDNS)\n\t\tlog.Printf(\"Forwarding %q DNS requests to %q\", cfg.DNS, cfg.F5Config.Object.DNS)\n\t\tlog.Printf(\"Default DNS servers: %q\", cfg.DNSServers)\n\t\tdns.Start(cfg, l.ErrChan, l.TunDown)\n\t}\n\n\treturn nil\n}\n\n// wait for pppd and config DNS and routes\nfunc (l *vpnLink) WaitAndConfig(cfg *config.Config) {\n\t// wait for ppp handshake completed\n\t<-l.pppUp\n\n\tl.Lock()\n\tdefer l.Unlock()\n\n\tvar err error\n\n\tif cfg.Driver != \"pppd\" {\n\t\t// create TUN\n\t\terr = l.createTunDevice()\n\t\tif err != nil {\n\t\t\tl.ErrChan <- err\n\t\t\treturn\n\t\t}\n\t\tdefer func() {\n\t\t\tif err != nil && l.iface != nil {\n\t\t\t\t// destroy interface on error\n\t\t\t\tif e := l.iface.Close(); e != nil {\n\t\t\t\t\tlog.Printf(\"error closing interface: %v\", e)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\terr = l.configureDNS(cfg)\n\tif err != nil {\n\t\tl.ErrChan <- err\n\t\treturn\n\t}\n\n\t// set routes\n\tlog.Printf(\"Setting routes on %s interface\", l.name)\n\n\t// set custom routes\n\troutes := cfg.Routes\n\tif routes == nil {\n\t\tlog.Printf(\"Applying routes, pushed from F5 VPN server\")\n\t\troutes = cfg.F5Config.Object.Routes\n\t}\n\n\t// exclude F5 gateway IPs\n\tfor _, dst := range l.serverIPs {\n\t\t// exclude only ipv4\n\t\tif v := dst.To4(); v != nil {\n\t\t\tlocal := &net.IPNet{\n\t\t\t\tIP:   v,\n\t\t\t\tMask: net.CIDRMask(32, 32),\n\t\t\t}\n\t\t\troutes.RemoveNet(local)\n\t\t}\n\t}\n\n\t// exclude local DNS servers, when they are not located inside the LAN\n\tfor _, v := range l.resolvHandler.GetOriginalDNS() {\n\t\tlocalDNS := &net.IPNet{\n\t\t\tIP:   v,\n\t\t\tMask: net.CIDRMask(32, 32),\n\t\t}\n\t\troutes.RemoveNet(localDNS)\n\t}\n\n\tvar gw net.IP\n\tif runtime.GOOS == \"windows\" {\n\t\t// windows requires both gateway and interface name\n\t\tgw = l.serverIPv4\n\t}\n\n\tl.routeHandler, err = route.New(l.name, routes.GetNetworks(), gw, 0)\n\tif err != nil {\n\t\tl.ErrChan <- err\n\t\treturn\n\t}\n\tl.routeHandler.Add()\n\n\tcolorlog.Print(color.HiGreenString(\"Connection established\"))\n}\n\n// restore config\nfunc (l *vpnLink) RestoreConfig(cfg *config.Config) {\n\tl.Lock()\n\tdefer l.Unlock()\n\n\tif l.routeHandler != nil {\n\t\tlog.Printf(\"Removing routes from %s interface\", l.name)\n\t\tl.routeHandler.Del()\n\t}\n\n\tif !cfg.DisableDNS {\n\t\tif l.resolvHandler != nil {\n\t\t\tlog.Printf(\"Restoring DNS settings\")\n\t\t\tl.resolvHandler.Restore()\n\t\t}\n\t}\n\n\tif cfg.Driver != \"pppd\" {\n\t\tif l.iface != nil {\n\t\t\terr := l.iface.Close()\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"error closing interface: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/link/pppd.go",
    "content": "package link\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/kayrus/gof5/pkg/util\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/hpcloud/tail\"\n\t\"github.com/zaninime/go-hdlc\"\n\t\"golang.org/x/net/ipv4\"\n)\n\n// TODO: handle \"fatal read pppd: read /dev/ptmx: input/output error\"\n// TODO: speed test vs native\n\nfunc (l *vpnLink) decodeHDLC(buf []byte, src string) {\n\ttmp := bytes.NewBuffer(buf)\n\tframe, err := hdlc.NewDecoder(tmp).ReadFrame()\n\tif err != nil {\n\t\tlog.Printf(\"fatal decode HDLC frame from %s: %s\", src, err)\n\t\treturn\n\t\t/*\n\t\t\tl.ErrChan <- fmt.Errorf(\"fatal decode HDLC frame from %s: %s\", source, err)\n\t\t\treturn\n\t\t*/\n\t}\n\tlog.Printf(\"Decoded %t prefix HDLC frame from %s:\\n%s\", frame.HasAddressCtrlPrefix, src, hex.Dump(frame.Payload))\n\th, err := ipv4.ParseHeader(frame.Payload[:])\n\tif err != nil {\n\t\tlog.Printf(\"fatal to parse TCP header from %s: %s\", src, err)\n\t\treturn\n\t\t/*\n\t\t\tl.ErrChan <- fmt.Errorf(\"fatal to parse TCP header: %s\", err)\n\t\t\treturn\n\t\t*/\n\t}\n\tlog.Printf(\"TCP: %s\", h)\n}\n\n// http->tun\nfunc (l *vpnLink) PppdHTTPToTun(pppd io.WriteCloser) {\n\tbuf := make([]byte, bufferSize)\n\tfor {\n\t\tselect {\n\t\tcase <-l.TunDown:\n\t\t\treturn\n\t\tdefault:\n\t\t\trn, err := l.HTTPConn.Read(buf)\n\t\t\tif err != nil {\n\t\t\t\tif err != io.EOF {\n\t\t\t\t\tl.ErrChan <- fmt.Errorf(\"fatal read http: %s\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif l.debug {\n\t\t\t\tl.decodeHDLC(buf[:rn], \"http\")\n\t\t\t\tlog.Printf(\"Read %d bytes from http:\\n%s\", rn, hex.Dump(buf[:rn]))\n\t\t\t}\n\t\t\twn, err := pppd.Write(buf[:rn])\n\t\t\tif err != nil {\n\t\t\t\tl.ErrChan <- fmt.Errorf(\"fatal write to pppd: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif l.debug {\n\t\t\t\tlog.Printf(\"Sent %d bytes to pppd\", wn)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// tun->http\nfunc (l *vpnLink) PppdTunToHTTP(pppd io.ReadCloser) {\n\tbuf := make([]byte, bufferSize)\n\tfor {\n\t\tselect {\n\t\tcase <-l.TunDown:\n\t\t\treturn\n\t\tdefault:\n\t\t\trn, err := pppd.Read(buf)\n\t\t\tif err != nil {\n\t\t\t\tif err != io.EOF {\n\t\t\t\t\tl.ErrChan <- fmt.Errorf(\"fatal read pppd: %s\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif l.debug {\n\t\t\t\tlog.Printf(\"Read %d bytes from pppd:\\n%s\", rn, hex.Dump(buf[:rn]))\n\t\t\t\tl.decodeHDLC(buf[:rn], \"pppd\")\n\t\t\t}\n\t\t\twn, err := l.HTTPConn.Write(buf[:rn])\n\t\t\tif err != nil {\n\t\t\t\tl.ErrChan <- fmt.Errorf(\"fatal write to http: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif l.debug {\n\t\t\t\tlog.Printf(\"Sent %d bytes to http\", wn)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// monitor the the ppp/pppd child process status\nfunc (l *vpnLink) CatchPPPDTermination(cmd *exec.Cmd) {\n\tdefer close(l.PppdErrChan)\n\tif err := cmd.Wait(); err != nil {\n\t\tl.PppdErrChan <- fmt.Errorf(\"%s process %v\", cmd.Path, err)\n\t\treturn\n\t}\n}\n\n// gracefully stop the ppp/pppd child\nfunc (l *vpnLink) StopPPPDChild(cmd *exec.Cmd) {\n\tif cmd != nil && cmd.Process != nil {\n\t\tcmd.Process.Signal(syscall.SIGTERM)\n\t\t<-l.PppdErrChan\n\t}\n}\n\n// pppd log parser\nfunc (l *vpnLink) PppdLogParser(stderr io.Reader) {\n\tscanner := bufio.NewScanner(stderr)\n\tfor scanner.Scan() {\n\t\tstr := scanner.Text()\n\t\tif strings.Contains(str, \"Using interface\") {\n\t\t\tif v := strings.FieldsFunc(str, util.SplitFunc); len(v) > 0 {\n\t\t\t\tl.name = v[len(v)-1]\n\t\t\t}\n\t\t}\n\t\tif strings.Contains(str, \"remote IP address\") {\n\t\t\tclose(l.pppUp)\n\t\t}\n\t\tcolorlog.Print(color.HiGreenString(str))\n\t}\n}\n\n// freebsd ppp log parser\n// TODO: talk directly via pppctl\n// /etc/ppp/ppp.conf should have `set server /var/run/ppp \"\" 0177`\nfunc (l *vpnLink) PppLogParser() {\n\tt, err := tail.TailFile(\"/var/log/ppp.log\", tail.Config{\n\t\tLocation: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},\n\t\tFollow:   true,\n\t\tLogger:   tail.DiscardingLogger,\n\t})\n\tif err != nil {\n\t\tl.ErrChan <- fmt.Errorf(\"failed to read ppp log: %s\", err)\n\t\treturn\n\t}\n\tfor line := range t.Lines {\n\t\tstr := line.Text\n\t\t// strip syslog prefix\n\t\tif v := strings.SplitN(str, \": \", 2); len(v) == 2 {\n\t\t\tstr = v[1]\n\t\t}\n\t\tif strings.Contains(str, \"Using interface\") {\n\t\t\tif v := strings.FieldsFunc(str, util.SplitFunc); len(v) > 0 {\n\t\t\t\tl.name = v[len(v)-1]\n\t\t\t}\n\t\t}\n\t\tif strings.Contains(str, \"IPCP: myaddr\") {\n\t\t\tclose(l.pppUp)\n\t\t}\n\t\tcolorlog.Print(color.HiGreenString(str))\n\t}\n}\n"
  },
  {
    "path": "pkg/util/util.go",
    "content": "package util\n\nfunc SplitFunc(c rune) bool {\n\treturn c == ' ' || c == '\\n' || c == '\\r'\n}\n\nfunc StrSliceContains(haystack []string, needle string) bool {\n\tfor _, s := range haystack {\n\t\tif s == needle {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  }
]