Full Code of kayrus/gof5 for AI

master 4769f04abb0e cached
34 files
121.2 KB
41.3k tokens
110 symbols
1 requests
Download .txt
Repository: kayrus/gof5
Branch: master
Commit: 4769f04abb0e
Files: 34
Total size: 121.2 KB

Directory structure:
gitextract_zfkw6760/

├── .github/
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── go-test.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── README.md
├── SIGNATURE.md
├── cmd/
│   └── gof5/
│       ├── gof5.manifest
│       ├── gof5_windows.syso
│       ├── main.go
│       ├── root_linux.go
│       ├── root_others.go
│       └── root_windows.go
├── go.mod
├── go.sum
├── org.freedesktop.resolve1.pkla
└── pkg/
    ├── client/
    │   ├── client.go
    │   ├── http.go
    │   ├── http_test.go
    │   └── logger.go
    ├── config/
    │   ├── config.go
    │   ├── types.go
    │   ├── wintun_other.go
    │   └── wintun_windows.go
    ├── cookie/
    │   └── cookie.go
    ├── dns/
    │   └── dns.go
    ├── link/
    │   ├── cmd_nix.go
    │   ├── cmd_windows.go
    │   ├── f5.go
    │   ├── link.go
    │   └── pppd.go
    └── util/
        └── util.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ master ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master ]
  schedule:
    - cron: '16 13 * * 1'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'go' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
        # Learn more about CodeQL language support at https://git.io/codeql-language-support

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v1
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v1

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v1


================================================
FILE: .github/workflows/go-test.yml
================================================
name: Go Unit Tests

on:
  push:
    branches:
      - master
  pull_request:

jobs:
  golang-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version-file: 'go.mod'
          cache: true

      - name: Run tests
        run: go test ./... -v


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  workflow_dispatch:
    inputs:
      tag:
      commit:
  push:
    tags:
      - v*

permissions:
  contents: write

jobs:
  release:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest, macos-13]
    steps:
      - uses: actions/checkout@v4
        if: github.event.inputs.commit != ''
        with:
          # checkout the commit if provided
          ref: ${{ github.event.inputs.commit }}
          # unshallow the repository to ensure all tags are available
          fetch-depth: 0

      - uses: actions/checkout@v4
        if: github.event.inputs.commit == ''
        with:
          # checkout the tag if provided, otherwise checkout the current ref
          ref: ${{ github.event.inputs.tag != '' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }}

      # workaround for Pro feature https://goreleaser.com/customization/nightlies/
      # create a dirty tag if the commit is not tagged
      - name: Get dirty git tag
        id: dirty_tag
        if: github.event.inputs.commit != ''
        shell: bash
        run: echo "tag=$(git tag --points-at HEAD | grep -q . || git describe --tags --always --abbrev=8 --dirty)" >> "$GITHUB_OUTPUT"
      - name: Set dirty git tag
        if: steps.dirty_tag.outputs.tag != ''
        run: git tag ${{ steps.dirty_tag.outputs.tag }}

      - uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'

      - name: Setup yq
        if: runner.os == 'Windows'
        uses: dcarbone/install-yq-action@v1

      # workaround for Pro feature https://goreleaser.com/customization/prebuilt/
      # and the inability to run `goreleaser release --id ${matrix.os}`
      - name: Copy goreleaser config to temp location
        run: cp .goreleaser.yml ${{ runner.temp }}/.goreleaser.yml
      # remove all builds except the one for the current OS
      - name: Override builds in copied config
        run: yq${{ runner.os == 'Windows' && '.exe' || '' }} -i eval '.builds |= map(select(.id == "${{ matrix.os }}"))' ${{ runner.temp }}/.goreleaser.yml

      - uses: goreleaser/goreleaser-action@v6
        with:
          args: release --clean --config ${{ runner.temp }}/.goreleaser.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
gopath
bin
cookies
routes.yaml


================================================
FILE: .goreleaser.yml
================================================
version: 2
builds:
  - id: ubuntu-latest
    main: ./cmd/gof5
    goos: [linux]
    goarch: [amd64]
    flags:
      - -trimpath
    ldflags:
      - -s -w -X main.Version=v{{ .Version }}
    env:
      - CGO_ENABLED=1

  - id: windows-latest
    main: ./cmd/gof5
    goos: [windows]
    goarch: [amd64]
    flags:
      - -trimpath
    ldflags:
      - -s -w -X main.Version=v{{ .Version }}
    env:
      - CGO_ENABLED=1

  - id: macos-13
    main: ./cmd/gof5
    goos: [darwin]
    goarch: [amd64]
    flags:
      - -trimpath
    ldflags:
      - -s -w -X main.Version=v{{ .Version }}
    env:
      - CGO_ENABLED=1

  - id: macos-latest
    main: ./cmd/gof5
    goos: [darwin]
    goarch: [arm64]
    flags:
      - -trimpath
    ldflags:
      - -s -w -X main.Version=v{{ .Version }}
    env:
      - CGO_ENABLED=1

archives:
  - formats: [binary]
    name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"

checksum:
  split: true

release:
  draft: true
  use_existing_draft: true
  replace_existing_draft: false

changelog:
  disable: true


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: Makefile
================================================
PKG:=github.com/kayrus/gof5
APP_NAME:=gof5
PWD:=$(shell pwd)
UID:=$(shell id -u)
VERSION:=$(shell git describe --tags --always --dirty="-dev")
GOOS:=$(shell go env GOOS)
LDFLAGS:=-X main.Version=$(VERSION) -w -s
GOOS:=$(strip $(shell go env GOOS))
GOARCHs:=$(strip $(shell go env GOARCH))

ifeq "$(GOOS)" "windows"
SUFFIX=.exe
endif

# CGO must be enabled
export CGO_ENABLED:=1

build: fmt vet
	$(foreach GOARCH,$(GOARCHs),$(shell GOARCH=$(GOARCH) go build -ldflags="$(LDFLAGS)" -trimpath -o bin/$(APP_NAME)_$(GOOS)_$(GOARCH)$(SUFFIX) ./cmd/gof5))

docker:
	docker pull golang:latest
	docker run -ti --rm -e GOCACHE=/tmp -v $(PWD):/$(APP_NAME) -u $(UID):$(UID) --workdir /$(APP_NAME) golang:latest make

fmt:
	gofmt -s -w cmd pkg

vet:
	go vet ./...

static:
	staticcheck ./cmd/... ./pkg/...

test:
	go test -v ./cmd/... ./pkg/...


================================================
FILE: README.md
================================================
# gof5

## Requirements

* an application must be executed under a privileged user

## Linux

If 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.
You need to adjust the binary capabilities:

```sh
$ sudo setcap cap_net_admin,cap_net_bind_service+ep /path/to/binary/gof5
```

For systemd-resolved you need to adjust PolicyKit Local Authority config, e.g. in Ubuntu:

```sh
$ cd gof5 # changedir to gof5 github repo
$ sudo cp org.freedesktop.resolve1.pkla /var/lib/polkit-1/localauthority/50-local.d/org.freedesktop.resolve1.pkla
$ sudo systemctl restart polkit.service
```

### Per user capabilities

If you want to have more granular restrictions to run gof5, you can allow only particular users to run it.

First of all add an entry before the `none  *` in a `/etc/security/capability.conf` file:

```
cap_net_admin,cap_net_bind_service %username%
```

where a `%username%` is a name of the user, which should get inherited `CAP_NET_ADMIN` and `CAP_NET_BIND_SERVICE` capabilities.

Adjust the binary flags to have inherited capabilities only:

```
$ sudo setcap cap_net_admin,cap_net_bind_service+i /path/to/binary/gof5
```

Check user's capabilities:

```
$ sudo -u %username% capsh --print | awk '/Current/{print $NF}'
cap_net_bind_service,cap_net_admin+i
```

gof5 should be executed using sudo even if you already logged in as this user:

```
$ sudo -u %username% /path/to/binary/gof5
```

## MacOS

On MacOS run the command below to avoid a `cannot be opened because the developer cannot be verified` warning:

```sh
xattr -d com.apple.quarantine ./path/to/gof5_darwin
```

## Windows

Windows version doesn't support `pppd` driver.

## ChromeOS

Developer mode should be enabled, since gof5 requires root privileges.
The binary should be placed inside the `/usr/share/oem` directory. Home directory in ChromeOS doesn't allow to have executables.
You need to restart shill with an option in order to allow tun interface creation: `sudo restart shill BLOCKED_DEVICES=tun0`.
Use the the `driver: pppd` config option if you don't want to restart shill.

## HOWTO

### Build from source

```sh
$ make # gmake in freebsd or mingw make for windows
# or build inside docker (linux version only)
$ make docker
```

### Run

```sh
# download the latest release
$ sudo gof5 --server server --username username --password token
```

Alternatively 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:

```js
document.cookie.match(/MRHSession=(.*?); /)[1]
```

Then specify it as an argument:

```sh
$ sudo gof5 --server server --session sessionID
```

When 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`).

Use `--close-session` flag to terminate an HTTPS VPN session on exit. Next startup will require a valid username/password.

Use `--select` to choose a VPN server from the list, known to a current server.

Use `--profile-index` to define a custom F5 VPN profile index.

### CA certificate and TLS keypair

Use options below to specify custom TLS parameters:

* `--ca-cert` - path to a custom CA certificate
* `--cert` - path to a user TLS certificate
* `--key` - path to a user TLS key

## Configuration

You can define an extra `~/.gof5/config.yaml` file with contents:

```yaml
# DNS proxy listen address, defaults to 127.0.0.245
# In BSD defaults to 127.0.0.1
# listenDNS: 127.0.0.1
# rewrite /etc/resolv.conf instead of renaming
# Linux only, required in cases when /etc/resolv.conf cannot be renamed
rewriteResolv: false
# experimental DTLSv1.2 support
# F5 BIG-IP server should have enabled DTLSv1.2 support
dtls: false
# TLS certificate check
insecureTLS: false
# Enable IPv6
ipv6: false
# driver specifies which tunnel driver to use.
# supported values are: wireguard or pppd.
# wireguard is default.
# pppd requires a pppd or ppp (in FreeBSD) binary
driver: wireguard
# When pppd driver is used, you can specify a list of extra pppd arguments
PPPdArgs: []
# disableDNS allows to completely disable DNS handling,
# i.e. don't alter system DNS (e.g. /etc/resolv.conf) at all
disableDNS: false
# TLS renegotiation support as defined in tls.RenegotiationSupport, disabled by default
renegotiation: RenegotiateNever
# A list of DNS zones to be resolved by VPN DNS servers
# When empty, every DNS query will be resolved by VPN DNS servers
dns:
- .corp.int.
- .corp.
# for reverse DNS lookup
- .in-addr.arpa.
# override DNS servers, provided by a VPN server profile
overrideDNS:
- 8.8.8.8
# override DNS search suffix, provided by a VPN server profile
overrideDNSSuffix:
- my.corp
# A list of subnets to be routed via VPN
# When not set, the routes pushed from F5 will be used
# Use "routes: []", if you don't want gof5 to manage routes at all
routes:
- 1.2.3.4
- 1.2.3.5/32
```


================================================
FILE: SIGNATURE.md
================================================
# Signature

* F5 client requests a token from a server: `/my.logon.php3?outform=xml&client_version=2.0&get_token=1`
* 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>`
* F5 client generates an **XML** with client parameters:

```xml
<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> // base64("test")
  <app_id/>
</agent_info>
```

Actual string:

`<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>`

* 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")))
* both **XML** and **signature** are base64 encoded and put into parameters:

`client_data = sprintf(str, "session=%s&device_info=%s&agent_result=%s&token=%s&signature=%s", "", base64(xml), "", token, signature)`

* The **client\_data** string generated above is also base64 encoded and then sent as a POST request to F5 `/my.policy`:

`post_request = sprintf(str, "client_data=%s", base64(client_data))`


================================================
FILE: cmd/gof5/gof5.manifest
================================================
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
    version="6.0.0.0"
    name="org.kayrus.gof5"
    type="win32"
/>
<description>gof5 requires Administrator privileges</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
        <requestedPrivileges>
            <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
        </requestedPrivileges>
    </security>
</trustInfo>
</assembly>


================================================
FILE: cmd/gof5/main.go
================================================
package main

import (
	"bufio"
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"

	"github.com/kayrus/gof5/pkg/client"
)

var (
	Version = "dev"
	info    = fmt.Sprintf("gof5 %s compiled with %s for %s/%s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
)

func fatal(err error) {
	if runtime.GOOS == "windows" {
		// Escalated privileges in windows opens a new terminal, and if there is an
		// error, it is impossible to see it. Thus we wait for user to press a button.
		log.Printf("%s, press enter to exit", err)
		bufio.NewReader(os.Stdin).ReadBytes('\n')
		os.Exit(1)
	}
	log.Fatal(err)
}

func main() {
	var version bool
	var opts client.Options

	flag.StringVar(&opts.Server, "server", "", "")
	flag.StringVar(&opts.Username, "username", "", "")
	flag.StringVar(&opts.Password, "password", "", "")
	flag.StringVar(&opts.SessionID, "session", "", "Reuse a session ID")
	flag.StringVar(&opts.CACert, "ca-cert", "", "Path to a custom CA certificate")
	flag.StringVar(&opts.Cert, "cert", "", "Path to a user TLS certificate")
	flag.StringVar(&opts.Key, "key", "", "Path to a user TLS key")
	flag.BoolVar(&opts.CloseSession, "close-session", false, "Close HTTPS VPN session on exit")
	flag.BoolVar(&opts.Debug, "debug", false, "Show debug logs")
	flag.BoolVar(&opts.Sel, "select", false, "Select a server from available F5 servers")
	flag.IntVar(&opts.ProfileIndex, "profile-index", 0, "If multiple VPN profiles are found chose profile n")
	flag.BoolVar(&version, "version", false, "Show version and exit cleanly")

	flag.Parse()

	if version {
		fmt.Println(info)
		os.Exit(0)
	}

	if opts.ProfileIndex < 0 {
		fatal(fmt.Errorf("profile-index cannot be negative"))
	}

	log.Print(info)

	if err := checkPermissions(); err != nil {
		fatal(err)
	}

	if flag.NArg() > 0 {
		if err := client.UrlHandlerF5Vpn(&opts, flag.Arg(0)); err != nil {
			fatal(err)
		}
	}

	if err := client.Connect(&opts); err != nil {
		fatal(err)
	}
}


================================================
FILE: cmd/gof5/root_linux.go
================================================
//go:build linux
// +build linux

package main

import (
	"fmt"
	"os"
	"strings"

	"kernel.org/pub/linux/libs/security/libcap/cap"
)

func checkCapability(c *cap.Set, capability cap.Value) error {
	// when "setcap capability+ep gof5" was used
	capable, err := c.GetFlag(cap.Effective, capability)
	if err != nil {
		return fmt.Errorf("failed to get process effective capability flag: %v", err)
	}
	if capable {
		return nil
	}

	// when "setcap capability+p gof5" or "setcap capability+i gof5" was used and a user has inheritable capability
	capable, err = c.GetFlag(cap.Permitted, capability)
	if err != nil {
		return fmt.Errorf("failed to get process permitted capability flag: %v", err)
	}
	if capable {
		if err = c.SetFlag(cap.Effective, true, capability); err != nil {
			return fmt.Errorf("permitted capability detected: failed to set effective %s capability flag: %v", strings.ToUpper(capability.String()), err)
		}
		if err = c.SetProc(); err != nil {
			return fmt.Errorf("permitted capability detected: failed to set effective %s capability: %v", strings.ToUpper(capability.String()), err)
		}
		return nil
	}

	return fmt.Errorf("cannot obtain effective %s capability", strings.ToUpper(capability.String()))
}

// TODO: detect cap_net_bind_service for DNS bind
func checkPermissions() error {
	// check root first
	if uid := os.Getuid(); uid == 0 {
		return nil
	}

	c := cap.GetProc()

	var err error
	capabilities := []cap.Value{
		cap.NET_ADMIN, // to create and manage tun interface
		// no need to run own DNS proxy, when systemd-resolved is used
		// cap.NET_BIND_SERVICE, // to bind DNS proxy
	}
	for _, capability := range capabilities {
		err = checkCapability(c, capability)
		if err != nil {
			break
		}
	}

	if err == nil {
		return nil
	}

	// no capabilities or "setcap capability+i gof5" was used and a user has no inheritable capability
	return fmt.Errorf("gof5 needs to run with CAP_NET_ADMIN capability or as root: %v", err)
}


================================================
FILE: cmd/gof5/root_others.go
================================================
//go:build !windows && !linux
// +build !windows,!linux

package main

import (
	"fmt"
	"os"
)

func checkPermissions() error {
	if uid := os.Getuid(); uid != 0 {
		return fmt.Errorf("gof5 needs to run as root")
	}
	return nil
}


================================================
FILE: cmd/gof5/root_windows.go
================================================
//go:build windows
// +build windows

package main

import (
	"fmt"

	"golang.org/x/sys/windows"
)

func checkPermissions() error {
	// https://github.com/golang/go/issues/28804#issuecomment-505326268
	var sid *windows.SID

	// https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
	err := windows.AllocateAndInitializeSid(
		&windows.SECURITY_NT_AUTHORITY,
		2,
		windows.SECURITY_BUILTIN_DOMAIN_RID,
		windows.DOMAIN_ALIAS_RID_ADMINS,
		0, 0, 0, 0, 0, 0,
		&sid)
	if err != nil {
		return fmt.Errorf("error while checking for elevated permissions: %s", err)
	}

	// We must free the sid to prevent security token leaks
	defer windows.FreeSid(sid)
	token := windows.Token(0)

	member, err := token.IsMember(sid)
	if err != nil {
		return fmt.Errorf("error while checking for elevated permissions: %s", err)
	}
	if !member {
		return fmt.Errorf("gof5 needs to run with administrator permissions")
	}

	return nil
}


================================================
FILE: go.mod
================================================
module github.com/kayrus/gof5

go 1.24.0

require (
	github.com/IBM/netaddr v1.5.0
	github.com/fatih/color v1.10.0
	github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
	github.com/hpcloud/tail v1.0.0
	github.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382
	github.com/manifoldco/promptui v0.8.0
	github.com/miekg/dns v1.1.40
	github.com/mitchellh/go-homedir v1.1.0
	github.com/pion/dtls/v2 v2.2.4
	github.com/zaninime/go-hdlc v1.1.1
	golang.org/x/net v0.47.0
	golang.org/x/sys v0.38.0
	gopkg.in/yaml.v2 v2.4.0
	kernel.org/pub/linux/libs/security/libcap/cap v1.2.48
)

require (
	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
	github.com/fsnotify/fsnotify v1.6.0 // indirect
	github.com/godbus/dbus/v5 v5.0.6 // indirect
	github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
	github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect
	github.com/mattn/go-colorable v0.1.8 // indirect
	github.com/mattn/go-isatty v0.0.12 // indirect
	github.com/pion/logging v0.2.2 // indirect
	github.com/pion/transport/v2 v2.0.0 // indirect
	github.com/pion/udp v0.1.4 // indirect
	github.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162 // indirect
	github.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f // indirect
	github.com/vishvananda/netlink v1.1.0 // indirect
	github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
	golang.org/x/crypto v0.45.0 // indirect
	golang.org/x/term v0.37.0 // indirect
	golang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71 // indirect
	golang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a // indirect
	gopkg.in/fsnotify.v1 v1.4.7 // indirect
	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
	kernel.org/pub/linux/libs/security/libcap/psx v1.2.48 // indirect
)


================================================
FILE: go.sum
================================================
github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0=
github.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382 h1:FGitiUIfcFXN472O3leZ5+n1w97D6+R3xJZxRQRy9es=
github.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382/go.mod h1:bU3N4PUqV+NW8pCT4gOS5Z7R1rqEq50q3vfP9hRhNj0=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162 h1:2zlAtlrum6lg2lMiUWznq04fDudBDajMFl94Zyis67Y=
github.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f h1:fKe0QdNJw68NO8iUdbC+jlwaA7/pA8sw0caZkpeXFTc=
github.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f/go.mod h1:VRI4lXkrUH5Cygl6mbG1BRUfMMoT2o8BkrtBDUAm+GU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zaninime/go-hdlc v1.1.1 h1:L0NBRiv49mSsCC+oSEmTbAcUntr8nseJpC+6pwYkBZ0=
github.com/zaninime/go-hdlc v1.1.1/go.mod h1:u/pMQOkSk+AucNZiuoil1ZKuO510qk8jn1JRyO7GR5w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71 h1:xANEpH9Q0hSSf/ogUZZg9yPBxo2x9Js+7LZoHI/EJRE=
golang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71/go.mod h1:RTjaYEQboNk7+2qfPGBotaMEh/5HIvmPZ6DIe10lTqI=
golang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a h1:4+nkXW+gJ+wq7ZqAspB4hAQymenjGwgN4O/IHn+kTm0=
golang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a/go.mod h1:EC9wJeih/xJQBE9kSNihR8I0WmojSTQRQMsCLMpzqYY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.48 h1:gW8VCEsPUwAp0/cW8CN2zfoqvz0+ijagsH2x+O2KlMM=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.48/go.mod h1:cs/AYPYd93hM59y4VPzpn4FP5TFgFoCcKtzlb0LM1c8=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.48 h1:5Oh8T4MP1+3KV2SvCBkCeGd97g7QHWMkTS7SrEme2bA=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.48/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=


================================================
FILE: org.freedesktop.resolve1.pkla
================================================
[Adding or changing system-wide resolved]
Identity=unix-group:netdev;unix-group:sudo
Action=org.freedesktop.resolve1.*
ResultAny=no
ResultInactive=no
ResultActive=yes


================================================
FILE: pkg/client/client.go
================================================
package client

import (
	"crypto/tls"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"os/signal"
	"runtime"
	"syscall"

	"github.com/kayrus/gof5/pkg/config"
	"github.com/kayrus/gof5/pkg/cookie"
	"github.com/kayrus/gof5/pkg/link"
)

type Options struct {
	config.Config
	Server        string
	Username      string
	Password      string
	SessionID     string
	CACert        string
	Cert          string
	Key           string
	CloseSession  bool
	Debug         bool
	Sel           bool
	Version       bool
	ProfileIndex  int
	ProfileName   string
	Renegotiation tls.RenegotiationSupport
}

func UrlHandlerF5Vpn(opts *Options, s string) error {
	u, err := url.Parse(s)
	if err != nil {
		return err
	}

	if u.Scheme != "f5-vpn" {
		return fmt.Errorf("invalid scheme %v expected f5-vpn", u.Scheme)
	}

	m, err := url.ParseQuery(u.RawQuery)
	if err != nil {
		return err
	}

	resourceTypes := m["resourcetype"]
	resourceNames := m["resourcename"]
	if len(resourceTypes) == len(resourceNames) {
		for i := range resourceTypes {
			if resourceTypes[i] == "network_access" {
				opts.ProfileName = resourceNames[i]
				break
			}
		}
	}

	opts.Server = m["server"][0]
	tokenUrl := fmt.Sprintf("%s://%s:%s/vdesk/get_sessid_for_token.php3", m["protocol"][0], opts.Server, m["port"][0])
	request, err := http.NewRequest(http.MethodGet, tokenUrl, nil)
	if err != nil {
		return err
	}
	otc := m["otc"]
	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	request.Header.Add("X-Access-Session-Token", otc[len(otc)-1])

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return err
	}

	opts.SessionID = response.Header.Get("X-Access-Session-ID")
	return nil
}

func Connect(opts *Options) error {
	if opts.Server == "" {
		fmt.Print("Enter server address: ")
		fmt.Scanln(&opts.Server)
	}

	u, err := url.Parse(opts.Server)
	if err != nil {
		return fmt.Errorf("failed to parse server hostname: %s", err)
	}
	if u.Scheme != "https" {
		u, err = url.Parse(fmt.Sprintf("https://%s", u.Host))
		if err != nil {
			return fmt.Errorf("failed to parse server hostname: %s", err)
		}
	}
	if u.Host == "" {
		u, err = url.Parse(fmt.Sprintf("https://%s", opts.Server))
		if err != nil {
			return fmt.Errorf("failed to parse server hostname: %s", err)
		}
		if u.Host == "" {
			return fmt.Errorf("failed to parse server hostname: %s", err)
		}
	}
	opts.Server = u.Host

	// read config
	cfg, err := config.ReadConfig(opts.Debug)
	if err != nil {
		return err
	}
	opts.Config = *cfg

	switch cfg.Renegotiation {
	case "RenegotiateOnceAsClient":
		opts.Renegotiation = tls.RenegotiateOnceAsClient
	case "RenegotiateFreelyAsClient":
		opts.Renegotiation = tls.RenegotiateFreelyAsClient
	case "RenegotiateNever", "":
		opts.Renegotiation = tls.RenegotiateNever
	default:
		return fmt.Errorf("unknown renegotiation value: '%s'", cfg.Renegotiation)
	}

	cookieJar, err := cookiejar.New(nil)
	if err != nil {
		return fmt.Errorf("failed to create cookie jar: %s", err)
	}

	client := &http.Client{Jar: cookieJar}
	client.CheckRedirect = checkRedirect(client)

	tlsConf, err := tlsConfig(opts, cfg.InsecureTLS)
	if err != nil {
		return fmt.Errorf("failed to build TLS config: %v", err)
	}
	transport := &http.Transport{
		TLSClientConfig: tlsConf,
	}
	if opts.Debug {
		client.Transport = &RoundTripper{
			Rt:     transport,
			Logger: &logger{},
		}
	} else {
		client.Transport = transport
	}

	// when server select list has been chosen
	if opts.Sel {
		u, err = getServersList(client, opts.Server)
		if err != nil {
			return err
		}
		opts.Server = u.Host
	}

	// read cookies
	cookie.ReadCookies(client, u, cfg, opts.SessionID)

	if len(client.Jar.Cookies(u)) == 0 {
		// need to login
		if err := login(client, opts.Server, &opts.Username, &opts.Password); err != nil {
			return fmt.Errorf("failed to login: %s", err)
		}
	} else {
		log.Printf("Reusing saved HTTPS VPN session for %s", u.Host)
	}

	resp, err := getProfiles(client, opts.Server)
	if err != nil {
		return fmt.Errorf("failed to get VPN profiles: %s", err)
	}

	if resp.StatusCode == 302 {
		// need to relogin
		_, err = io.Copy(ioutil.Discard, resp.Body)
		if err != nil {
			return fmt.Errorf("failed to read response body: %s", err)
		}
		resp.Body.Close()

		if err := login(client, opts.Server, &opts.Username, &opts.Password); err != nil {
			return fmt.Errorf("failed to login: %s", err)
		}

		// new request
		resp, err = getProfiles(client, opts.Server)
		if err != nil {
			return fmt.Errorf("failed to get VPN profiles: %s", err)
		}
	}

	if resp.StatusCode != 200 {
		return fmt.Errorf("wrong response code on profiles get: %d", resp.StatusCode)
	}

	profile, err := parseProfile(resp.Body, opts.ProfileIndex, opts.ProfileName)
	if err != nil {
		return fmt.Errorf("failed to parse VPN profiles: %s", err)
	}

	// read config, returned by F5
	cfg.F5Config, err = getConnectionOptions(client, opts, profile)
	if err != nil {
		return fmt.Errorf("failed to get VPN connection options: %s", err)
	}

	// save cookies
	if err := cookie.SaveCookies(client, u, cfg); err != nil {
		return fmt.Errorf("failed to save cookies: %s", err)
	}

	// close HTTPS VPN session
	// next VPN connection will require credentials to auth
	if opts.CloseSession {
		defer closeVPNSession(client, opts.Server)
	}

	// TLS
	l, err := link.InitConnection(opts.Server, cfg, tlsConf)
	if err != nil {
		return err
	}
	defer l.HTTPConn.Close()

	cmd := link.Cmd(cfg)

	termChan := make(chan os.Signal, 1)
	signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE, syscall.SIGHUP)

	// set routes and DNS after the PPP/TUN is up
	go l.WaitAndConfig(cfg)

	// 1. stop ppp/pppd child at the very end
	defer l.StopPPPDChild(cmd)
	// 0. restore the config first
	defer l.RestoreConfig(cfg)

	if cfg.Driver == "pppd" {
		if runtime.GOOS == "freebsd" {
			// ppp log parser
			go l.PppLogParser()
		} else {
			/*
				// read file descriptor 3
				stderr, w, err := os.Pipe()
				cmd.ExtraFiles = []*os.File{w}
			*/
			stderr, err := cmd.StderrPipe()
			if err != nil {
				return fmt.Errorf("cannot allocate stderr pipe: %s", err)
			}
			// pppd log parser
			go l.PppdLogParser(stderr)
		}

		stdin, err := cmd.StdinPipe()
		if err != nil {
			return fmt.Errorf("cannot allocate stdin pipe: %s", err)
		}
		stdout, err := cmd.StdoutPipe()
		if err != nil {
			return fmt.Errorf("cannot allocate stdout pipe: %s", err)
		}

		err = cmd.Start()
		if err != nil {
			return fmt.Errorf("failed to start pppd: %s", err)
		}

		// catch ppp/pppd child termination
		go l.CatchPPPDTermination(cmd)

		// pppd http->tun go routine
		go l.PppdHTTPToTun(stdin)

		// pppd tun->http go routine
		go l.PppdTunToHTTP(stdout)
	} else {
		// http->tun go routine
		go l.HttpToTun()

		// tun->http go routine
		go l.TunToHTTP()
	}

	select {
	case sig := <-termChan:
		log.Printf("received %s signal, exiting", sig)
	case err = <-l.ErrChan:
		// error received
	case err = <-l.PppdErrChan:
		// ppp/pppd child error received
	}

	// notify tun readers and writes to stop
	close(l.TunDown)

	return err
}


================================================
FILE: pkg/client/http.go
================================================
package client

import (
	"bytes"
	"crypto/hmac"
	"crypto/md5"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"regexp"
	"strings"

	"github.com/kayrus/gof5/pkg/config"

	"github.com/howeyc/gopass"
	"github.com/manifoldco/promptui"
	"github.com/mitchellh/go-homedir"
)

const (
	userAgent        = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1a2pre) Gecko/2008073000 Shredder/3.0a2pre ThunderBrowse/3.2.1.8"
	androidUserAgent = "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"
)

func tlsConfig(opts *Options, insecure bool) (*tls.Config, error) {
	config := &tls.Config{
		InsecureSkipVerify: insecure,
		Renegotiation:      opts.Renegotiation,
	}

	if opts.CACert != "" {
		caCert, err := readFile(opts.CACert)
		if err != nil {
			return nil, err
		}
		config.RootCAs = x509.NewCertPool()
		config.RootCAs.AppendCertsFromPEM(caCert)
	}

	if opts.Cert != "" && opts.Key != "" {
		crt, err := readFile(opts.Cert)
		if err != nil {
			return nil, err
		}
		key, err := readFile(opts.Key)
		if err != nil {
			return nil, err
		}

		cert, err := tls.X509KeyPair(crt, key)
		if err != nil {
			return nil, err
		}

		config.Certificates = []tls.Certificate{cert}
	}

	return config, nil
}

func readFile(path string) ([]byte, error) {
	if len(path) == 0 {
		return nil, nil
	}

	if path[0] == '~' {
		var err error
		path, err = homedir.Expand(path)
		if err != nil {
			return nil, err
		}
	}

	if _, err := os.Stat(path); err != nil {
		return nil, err
	}

	content, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}

	return bytes.TrimSpace(content), nil
}

func checkRedirect(c *http.Client) func(*http.Request, []*http.Request) error {
	return func(req *http.Request, via []*http.Request) error {
		if req.URL.Path == "/my.logout.php3" || req.URL.Path == "/vdesk/hangup.php3" || req.URL.Query().Get("errorcode") != "" {
			// clear cookies
			var err error
			c.Jar, err = cookiejar.New(nil)
			if err != nil {
				return fmt.Errorf("failed to create cookie jar: %s", err)
			}
			return http.ErrUseLastResponse
		}
		return nil
	}
}

// 64-byte HMAC key
var hmacKey, _ = hex.DecodeString(
	"4342a2ee5e546d98bd24e014218c8b8d" +
	"c18531bd538c4694b720043435367edb" +
	"f5dd67a9f6da42b58d28b27710c39b1a" +
	"b4cb386acdae4e08bd328d8a45b0b082")

func generateClientData(cData config.ClientData) (string, error) {
	info := config.AgentInfo{
		Type:       "standalone",
		Version:    "2.0",
		Platform:   "Linux",
		CPU:        "x64",
		LandingURI: "/",
		Hostname:   "test",
	}

	data, err := xml.Marshal(info)
	if err != nil {
		return "", fmt.Errorf("failed to marshal agent info: %s", err)
	}

	if info.AppID == "" {
		r := regexp.MustCompile("></agent_info>")
		data = []byte(r.ReplaceAllString(string(data), "><app_id></app_id></agent_info>"))
	}

	values := &bytes.Buffer{}
	values.WriteString("session=&")
	values.WriteString("device_info=" + base64.StdEncoding.EncodeToString(data) + "&")
	values.WriteString("agent_result=&")
	values.WriteString("token=" + cData.Token)

	// HMAC-MD5 with the 64-byte key
	h := hmac.New(md5.New, hmacKey)
	h.Write(values.Bytes())
	sig := base64.StdEncoding.EncodeToString(h.Sum(nil))

	values.WriteString("&signature=" + sig)

	return base64.StdEncoding.EncodeToString(values.Bytes()), nil
}

func loginSignature(c *http.Client, server string, _, _ *string) error {
	log.Printf("Logging in...")
	req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/my.logon.php3?outform=xml&client_version=2.0&get_token=1", server), nil)
	if err != nil {
		return err
	}
	req.Proto = "HTTP/1.0"
	req.Header.Set("User-Agent", androidUserAgent)
	resp, err := c.Do(req)
	if err != nil {
		return err
	}

	var cData config.ClientData
	dec := xml.NewDecoder(resp.Body)
	err = dec.Decode(&cData)
	resp.Body.Close()
	if err != nil {
		return err
	}

	clientData, err := generateClientData(cData)
	if err != nil {
		return err
	}

	req, err = http.NewRequest("POST", fmt.Sprintf("https://%s%s", server, cData.RedirectURL), strings.NewReader("client_data="+clientData))
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", androidUserAgent)
	req.Header.Set("Pragma", "no-cache")
	req.Header.Set("Cache-Control", "no-cache")
	req.Header.Set("Upgrade-Insecure-Requests", "1")
	req.Header.Set("Origin", "null")
	req.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")
	req.Header.Set("content-type", "application/x-www-form-urlencoded")
	req.Header.Set("X-Requested-With", "com.f5.edge.client_ics")
	req.Header.Set("Sec-Fetch-Site", "none")
	req.Header.Set("Sec-Fetch-Mode", "navigate")
	req.Header.Set("Sec-Fetch-User", "?1")
	req.Header.Set("Sec-Fetch-Dest", "document")
	req.Header.Set("Accept-Encoding", "gzip, deflate")
	req.Header.Set("Accept-Language", "en-US;q=0.9,en;q=0.8")

	resp, err = c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode == 302 {
		return fmt.Errorf("login failed")
	}

	_, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	return nil
}

func login(c *http.Client, server string, username, password *string) error {
	if *username == "" {
		fmt.Print("Enter VPN username: ")
		fmt.Scanln(username)
	}
	if *password == "" {
		fmt.Print("Enter VPN password: ")
		v, err := gopass.GetPasswd()
		if err != nil {
			return fmt.Errorf("failed to read password: %s", err)
		}
		*password = string(v)
	}

	log.Printf("Logging in...")
	req, err := http.NewRequest("GET", fmt.Sprintf("https://%s", server), nil)
	if err != nil {
		return err
	}
	req.Proto = "HTTP/1.0"
	req.Header.Set("User-Agent", userAgent)
	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	_, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	resp.Body.Close()

	data := url.Values{}
	data.Set("username", *username)
	data.Add("password", *password)
	data.Add("vhost", "standard")
	req, err = http.NewRequest("POST", fmt.Sprintf("https://%s/my.policy?outform=xml", server), strings.NewReader(data.Encode()))
	if err != nil {
		return err
	}
	req.Header.Set("Referer", fmt.Sprintf("https://%s/my.policy", server))
	req.Header.Set("User-Agent", userAgent)
	resp, err = c.Do(req)
	if err != nil {
		return err
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	resp.Body.Close()

	/*
		if resp.StatusCode == 302 && resp.Header.Get("Location") == "/my.policy" {
			return nil
		}
	*/

	// TODO: parse response 302 location and error code
	if resp.StatusCode == 302 || bytes.Contains(body, []byte("Session Expired/Timeout")) || bytes.Contains(body, []byte("The username or password is not correct")) {
		return fmt.Errorf("wrong credentials")
	}

	return nil
}

func parseProfile(reader io.ReadCloser, profileIndex int, profileName string) (string, error) {
	var profiles config.Profiles
	dec := xml.NewDecoder(reader)
	err := dec.Decode(&profiles)
	reader.Close()
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal a response: %s", err)
	}

	if profiles.Type == "VPN" {
		prfls := make([]string, len(profiles.Favorites))
		for i, p := range profiles.Favorites {
			if profileName != "" && profileName == p.Name {
				profileIndex = i
			}
			prfls[i] = fmt.Sprintf("%d:%s", i, p.Name)
		}
		log.Printf("Found F5 VPN profiles: %q", prfls)

		if profileIndex >= len(profiles.Favorites) {
			return "", fmt.Errorf("profile %q index is out of range", profileIndex)
		}
		log.Printf("Using %q F5 VPN profile", profiles.Favorites[profileIndex].Name)
		return profiles.Favorites[profileIndex].Params, nil
	}

	return "", fmt.Errorf("VPN profile was not found")
}

func getProfiles(c *http.Client, server string) (*http.Response, error) {
	req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/vdesk/vpn/index.php3?outform=xml&client_version=2.0", server), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to build a request: %s", err)
	}
	req.Header.Set("User-Agent", userAgent)
	return c.Do(req)
}

func getConnectionOptions(c *http.Client, opts *Options, profile string) (*config.Favorite, error) {
	req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/vdesk/vpn/connect.php3?%s&outform=xml&client_version=2.0", opts.Server, profile), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to build a request: %s", err)
	}
	req.Header.Set("User-Agent", userAgent)
	resp, err := c.Do(req)

	if err != nil {
		log.Printf("Failed to read a request: %s", err)
		log.Printf("Override link DNS values from config")
		return &config.Favorite{
			Object: config.Object{
				SessionID: opts.SessionID,
				DNS:       opts.Config.OverrideDNS,
				DNSSuffix: opts.Config.OverrideDNSSuffix,
			},
		}, nil
	}

	// parse profile
	var favorite config.Favorite
	dec := xml.NewDecoder(resp.Body)
	err = dec.Decode(&favorite)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal a response: %s", err)
	}

	// override link options
	if favorite.Object.SessionID == "" {
		favorite.Object.SessionID = opts.SessionID
	}
	if len(opts.Config.OverrideDNS) > 0 {
		favorite.Object.DNS = opts.Config.OverrideDNS
	}
	if len(opts.Config.OverrideDNSSuffix) > 0 {
		favorite.Object.DNSSuffix = opts.Config.OverrideDNSSuffix
	}

	return &favorite, nil
}

func closeVPNSession(c *http.Client, server string) {
	// close session
	r, err := http.NewRequest("GET", fmt.Sprintf("https://%s/vdesk/hangup.php3?hangup_error=1", server), nil)
	if err != nil {
		log.Printf("Failed to create a request to close the VPN session %s", err)
	}
	resp, err := c.Do(r)
	if err != nil {
		log.Printf("Failed to close the VPN session %s", err)
	}
	defer resp.Body.Close()
}

func getServersList(c *http.Client, server string) (*url.URL, error) {
	r, err := http.NewRequest("GET", fmt.Sprintf("https://%s/pre/config.php", server), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create a request to get servers list: %s", err)
	}
	resp, err := c.Do(r)
	if err != nil {
		return nil, fmt.Errorf("failed to request servers list: %s", err)
	}

	var s config.PreConfigProfile
	dec := xml.NewDecoder(resp.Body)
	err = dec.Decode(&s)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal servers list: %s", err)
	}

	prompt := promptui.Select{
		Label: "Select Server",
		Items: s.Servers,
	}

	i, _, err := prompt.Run()
	if err != nil {
		return nil, fmt.Errorf("prompt failed: %s", err)
	}

	u, err := url.Parse(s.Servers[i].Address)
	if err != nil {
		return nil, fmt.Errorf("failed to parse server hostname: %s", err)
	}

	// if scheme is not set, assume https
	if u.Scheme == "" {
		u, err = url.Parse("https://" + s.Servers[i].Address)
		if err != nil {
			return nil, fmt.Errorf("failed to parse server hostname: %s", err)
		}
	}

	return u, nil
}


================================================
FILE: pkg/client/http_test.go
================================================
package client

import (
	"encoding/xml"
	"testing"

	"github.com/kayrus/gof5/pkg/config"
)

func TestSignature(t *testing.T) {
	s, err := generateClientData(config.ClientData{Token: "1"})
	if err != nil {
		t.Errorf("Signature is wrong: %s", err)
	}

	expected := "c2Vzc2lvbj0mZGV2aWNlX2luZm89UEdGblpXNTBYMmx1Wm04K1BIUjVjR1UrYzNSaGJtUmhiRzl1WlR3dmRIbHdaVDQ4ZG1WeWMybHZiajR5TGpBOEwzWmxjbk5wYjI0K1BIQnNZWFJtYjNKdFBreHBiblY0UEM5d2JHRjBabTl5YlQ0OFkzQjFQbmcyTkR3dlkzQjFQanhxWVhaaGMyTnlhWEIwUG01dlBDOXFZWFpoYzJOeWFYQjBQanhoWTNScGRtVjRQbTV2UEM5aFkzUnBkbVY0UGp4d2JIVm5hVzQrYm04OEwzQnNkV2RwYmo0OGJHRnVaR2x1WjNWeWFUNHZQQzlzWVc1a2FXNW5kWEpwUGp4c2IyTnJaV1J0YjJSbFBtNXZQQzlzYjJOclpXUnRiMlJsUGp4b2IzTjBibUZ0WlQ1a1IxWjZaRUU5UFR3dmFHOXpkRzVoYldVK1BHRndjRjlwWkQ0OEwyRndjRjlwWkQ0OEwyRm5aVzUwWDJsdVptOCsmYWdlbnRfcmVzdWx0PSZ0b2tlbj0xJnNpZ25hdHVyZT00c1krcFFkM3pyUTVjMkZsNUJ3a0JnPT0="
	if s != expected {
		t.Errorf("Client data doesn't correspond to expected: %s", s)
	}
}

func TestUnmarshal(t *testing.T) {
	// parse https://f5.com/pre/config.php
	b := []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>`)
	var s config.PreConfigProfile
	if err := xml.Unmarshal(b, &s); err != nil {
		t.Errorf("failed to unmarshal a response: %s", err)
	}
}


================================================
FILE: pkg/client/logger.go
================================================
package client

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
)

// Logger is an interface representing the Logger struct
type Logger interface {
	RequestPrintf(format string, args ...interface{})
	ResponsePrintf(format string, args ...interface{})
}

type logger struct {
	RequestID string
}

func (lg logger) RequestPrintf(format string, args ...interface{}) {
	for _, v := range strings.Split(fmt.Sprintf(format, args...), "\n") {
		log.Printf("-> %s", v)
	}
}

func (lg logger) ResponsePrintf(format string, args ...interface{}) {
	for _, v := range strings.Split(fmt.Sprintf(format, args...), "\n") {
		log.Printf("<- %s", v)
	}
}

// noopLogger is a default noop logger satisfies the Logger interface
type noopLogger struct{}

// Printf is a default noop method
func (noopLogger) RequestPrintf(format string, args ...interface{}) {}

// Printf is a default noop method
func (noopLogger) ResponsePrintf(format string, args ...interface{}) {}

// RoundTripper satisfies the http.RoundTripper interface and is used to
// customize the default http client RoundTripper
type RoundTripper struct {
	// Default http.RoundTripper
	Rt http.RoundTripper
	// If Logger is not nil, then RoundTrip method will debug the JSON
	// requests and responses
	Logger Logger
}

// formatHeaders converts standard http.Header type to a string with separated headers.
func (rt *RoundTripper) formatHeaders(headers http.Header, separator string) string {
	result := make([]string, len(headers))

	i := 0
	for header, data := range headers {
		result[i] = fmt.Sprintf("%s: %s", header, strings.Join(data, " "))
		i++
	}

	return strings.Join(result, separator)
}

// RoundTrip performs a round-trip HTTP request and logs relevant information about it.
func (rt *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
	defer func() {
		if request.Body != nil {
			request.Body.Close()
		}
	}()

	var err error

	if rt.Logger != nil {
		rt.log().RequestPrintf("URL: %s %s", request.Method, request.URL)
		rt.log().RequestPrintf("Headers:\n%s", rt.formatHeaders(request.Header, "\n"))

		if request.Body != nil {
			request.Body, err = rt.logRequest(request.Body, request.Header.Get("Content-Type"))
			if err != nil {
				return nil, err
			}
		}
	}

	// this is concurrency safe
	ort := rt.Rt
	if ort == nil {
		return nil, fmt.Errorf("rt RoundTripper is nil, aborting")
	}
	response, err := ort.RoundTrip(request)

	if response == nil {
		if rt.Logger != nil {
			rt.log().ResponsePrintf("Connection error, retries exhausted. Aborting")
		}
		err = fmt.Errorf("connection error, retries exhausted. Aborting. Last error was: %s", err)
		return nil, err
	}

	if rt.Logger != nil {
		rt.log().ResponsePrintf("Code: %d", response.StatusCode)
		rt.log().ResponsePrintf("Headers:\n%s", rt.formatHeaders(response.Header, "\n"))

		response.Body, err = rt.logResponse(response.Body, response.Header.Get("Content-Type"))
	}

	return response, err
}

// logRequest will log the HTTP Request details.
// If the body is JSON, it will attempt to be pretty-formatted.
func (rt *RoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
	var bs bytes.Buffer
	defer original.Close()

	if _, err := io.Copy(&bs, original); err != nil {
		return nil, err
	}

	rt.log().RequestPrintf("Body: %s", bs.String())

	return ioutil.NopCloser(bytes.NewReader(bs.Bytes())), nil
}

// logResponse will log the HTTP Response details.
// If the body is JSON, it will attempt to be pretty-formatted.
func (rt *RoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
	var bs bytes.Buffer
	defer original.Close()

	if _, err := io.Copy(&bs, original); err != nil {
		return nil, err
	}

	rt.log().ResponsePrintf("Body: %s", bs.String())

	return ioutil.NopCloser(bytes.NewReader(bs.Bytes())), nil
}

func (rt *RoundTripper) log() Logger {
	// this is concurrency safe
	l := rt.Logger
	if l == nil {
		// noop is used, when logger pointer has been set to nil
		return &noopLogger{}
	}
	return l
}


================================================
FILE: pkg/config/config.go
================================================
package config

import (
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"os"
	"os/user"
	"path/filepath"
	"runtime"
	"strconv"

	"github.com/kayrus/gof5/pkg/util"

	"gopkg.in/yaml.v2"
)

const (
	configDir  = ".gof5"
	configName = "config.yaml"
)

var (
	defaultDNSListenAddr = net.IPv4(127, 0, 0, 0xf5).To4()
	// BSD systems don't support listeniing on 127.0.0.1+N
	defaultBSDDNSListenAddr = net.IPv4(127, 0, 0, 1).To4()
	supportedDrivers        = []string{"wireguard", "pppd"}
)

func ReadConfig(debug bool) (*Config, error) {
	var err error
	var usr *user.User

	// resolve sudo user ID
	if id, sudoUID := os.Geteuid(), os.Getenv("SUDO_UID"); id == 0 && sudoUID != "" {
		usr, err = user.LookupId(sudoUID)
		if err != nil {
			log.Printf("failed to lookup user ID: %s", err)
			if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
				usr, err = user.Lookup(sudoUser)
				if err != nil {
					return nil, fmt.Errorf("failed to lookup user name: %s", err)
				}
			}
		}
	} else {
		// detect home directory
		usr, err = user.Current()
		if err != nil {
			return nil, fmt.Errorf("failed to detect home directory: %s", err)
		}
	}
	configPath := filepath.Join(usr.HomeDir, configDir)

	var uid, gid int
	// windows preserves the original user parameters, no need to detect uid/gid
	if runtime.GOOS != "windows" {
		uid, err = strconv.Atoi(usr.Uid)
		if err != nil {
			return nil, fmt.Errorf("failed to convert %q UID to integer: %s", usr.Uid, err)
		}
		gid, err = strconv.Atoi(usr.Gid)
		if err != nil {
			return nil, fmt.Errorf("failed to convert %q GID to integer: %s", usr.Uid, err)
		}
	}

	if _, err := os.Stat(configPath); os.IsNotExist(err) {
		log.Printf("%q directory doesn't exist, creating...", configPath)
		if err := os.Mkdir(configPath, 0700); err != nil {
			return nil, fmt.Errorf("failed to create %q config directory: %s", configPath, err)
		}
		// windows preserves the original user parameters, no need to chown
		if runtime.GOOS != "windows" {
			if err := os.Chown(configPath, uid, gid); err != nil {
				return nil, fmt.Errorf("failed to set an owner for the %q config directory: %s", configPath, err)
			}
		}
	} else if err != nil {
		return nil, fmt.Errorf("failed to get %q directory stat: %s", configPath, err)
	}

	cfg := &Config{}
	// read config file
	// if config doesn't exist, use defaults
	if raw, err := ioutil.ReadFile(filepath.Join(configPath, configName)); err == nil {
		if err = yaml.Unmarshal(raw, cfg); err != nil {
			return nil, fmt.Errorf("cannot parse %s file: %v", configName, err)
		}
	} else {
		log.Printf("Cannot read config file: %s", err)
	}

	// set default driver
	if cfg.Driver == "" {
		cfg.Driver = "wireguard"
	}

	if cfg.Driver == "wireguard" {
		if err := checkWinTunDriver(); err != nil {
			return nil, err
		}
	}

	if cfg.Driver == "pppd" && runtime.GOOS == "windows" {
		return nil, fmt.Errorf("pppd driver is not supported in Windows")
	}

	if !util.StrSliceContains(supportedDrivers, cfg.Driver) {
		return nil, fmt.Errorf("%q driver is unsupported, supported drivers are: %q", cfg.Driver, supportedDrivers)
	}

	if cfg.ListenDNS == nil {
		switch runtime.GOOS {
		case "freebsd",
			"darwin":
			cfg.ListenDNS = defaultBSDDNSListenAddr
		default:
			cfg.ListenDNS = defaultDNSListenAddr
		}
	}

	cfg.Path = configPath
	cfg.Uid = uid
	cfg.Gid = gid

	cfg.Debug = debug

	return cfg, nil
}


================================================
FILE: pkg/config/types.go
================================================
package config

import (
	"encoding/base64"
	"encoding/xml"
	"fmt"
	"log"
	"net"
	"net/url"
	"strings"

	"github.com/kayrus/gof5/pkg/util"

	"github.com/IBM/netaddr"
)

type Config struct {
	Debug             bool           `yaml:"-"`
	Driver            string         `yaml:"driver"`
	ListenDNS         net.IP         `yaml:"-"`
	DNS               []string       `yaml:"dns"`
	OverrideDNS       []net.IP       `yaml:"-"`
	OverrideDNSSuffix []string       `yaml:"overrideDNSSuffix"`
	Routes            *netaddr.IPSet `yaml:"-"`
	PPPdArgs          []string       `yaml:"pppdArgs"`
	InsecureTLS       bool           `yaml:"insecureTLS"`
	DTLS              bool           `yaml:"dtls"`
	IPv6              bool           `yaml:"ipv6"`
	// completely disable DNS servers handling
	DisableDNS bool `yaml:"disableDNS"`
	// rewrite /etc/resolv.conf instead of renaming
	// required in ChromeOS, where /etc/resolv.conf cannot be renamed
	RewriteResolv bool `yaml:"rewriteResolv"`
	// tls regeneration, tls.RenegotiateNever by default
	Renegotiation string `yaml:"renegotiation"`
	// list of detected local DNS servers
	DNSServers []net.IP `yaml:"-"`
	// config path
	Path string `yaml:"-"`
	// current user or sudo user UID
	Uid int `yaml:"-"`
	// current user or sudo user GID
	Gid int `yaml:"-"`
	// Config, returned by F5
	F5Config *Favorite `yaml:"-"`
}

func (r *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
	type tmp Config
	var s struct {
		tmp
		ListenDNS   *string  `yaml:"listenDNS"`
		Routes      []string `yaml:"routes"`
		PPPdArgs    []string `yaml:"pppdArgs"`
		OverrideDNS []string `yaml:"overrideDNS"`
	}

	if err := unmarshal(&s.tmp); err != nil {
		return err
	}

	if err := unmarshal(&s); err != nil {
		return err
	}

	*r = Config(s.tmp)

	if s.ListenDNS != nil {
		r.ListenDNS = net.ParseIP(*s.ListenDNS)
	}

	r.Routes = new(netaddr.IPSet)
	if s.Routes != nil {
		// handle the case, when routes is an empty list
		parsedCIDRs, err := parseCIDRs(s.Routes, net.IPv4len)
		if err != nil {
			return err
		}
		r.Routes = subnetsToIPSet(parsedCIDRs)
	}

	if len(s.OverrideDNS) > 0 {
		r.OverrideDNS = processIPs(strings.Join(s.OverrideDNS, " "), net.IPv4len)
	}

	// default pppd arguments
	r.PPPdArgs = []string{
		"logfd", "2",
		"noauth",
		"nodetach",
		"passive",
		"ipcp-accept-local",
		"ipcp-accept-remote",
		"notty", // use default stdin/stdout
		"nodefaultroute",
		// nocompression
		"novj",
		"novjccomp",
		"noaccomp",
		"noccp",
		"nopcomp",
		"nopredictor1",
		"nodeflate", // Protocol-Reject for 'Compression Control Protocol' (0x80fd) received
		"nobsdcomp", // Protocol-Reject for 'Compression Control Protocol' (0x80fd) received
	}
	if len(s.PPPdArgs) > 0 {
		// extra pppd args
		r.PPPdArgs = append(r.PPPdArgs, s.PPPdArgs...)
	}

	return nil
}

type Favorite struct {
	Object Object `xml:"object"`
}

type Bool bool

func (b Bool) String() string {
	if b {
		return "yes"
	}
	return "no"
}

func (b Bool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	return e.EncodeElement(b.String(), start)
}

func strToBool(s string) (Bool, error) {
	switch v := strings.ToLower(s); v {
	case "yes":
		return true, nil
	case "no", "":
		return false, nil
	}
	return false, fmt.Errorf("cannot parse boolean: %s", s)
}

// TODO: unmarshal for bool

type Object struct {
	SessionID                      string         `xml:"Session_ID"`
	IPv4                           Bool           `xml:"IPV4_0"`
	IPv6                           Bool           `xml:"IPV6_0"`
	UrZ                            string         `xml:"ur_Z"`
	HDLCFraming                    Bool           `xml:"-"`
	Host                           string         `xml:"host0"`
	Port                           string         `xml:"port0"`
	TunnelHost                     string         `xml:"tunnel_host0"`
	TunnelPort                     string         `xml:"tunnel_port0"`
	Add2Hosts                      string         `xml:"Add2Hosts0"`
	DNSRegisterConnection          int            `xml:"DNSRegisterConnection0"`
	DNSUseDNSSuffixForRegistration int            `xml:"DNSUseDNSSuffixForRegistration0"`
	SplitTunneling                 int            `xml:"SplitTunneling0"`
	DNSSPlit                       string         `xml:"DNS_SPLIT0"`
	TunnelDTLS                     bool           `xml:"tunnel_dtls"`
	TunnelPortDTLS                 string         `xml:"tunnel_port_dtls"`
	AllowLocalSubnetAccess         bool           `xml:"AllowLocalSubnetAccess0"`
	AllowLocalDNSServersAccess     bool           `xml:"AllowLocalDNSServersAccess0"`
	AllowLocalDHCPAccess           bool           `xml:"AllowLocalDHCPAccess0"`
	DNS                            []net.IP       `xml:"-"`
	DNS6                           []net.IP       `xml:"-"`
	ExcludeSubnets                 []*net.IPNet   `xml:"-"`
	Routes                         *netaddr.IPSet `xml:"-"`
	ExcludeSubnets6                []*net.IPNet   `xml:"-"`
	Routes6                        *netaddr.IPSet `xml:"-"`
	TrafficControl                 TrafficControl `xml:"-"`
	DNSSuffix                      []string       `xml:"-"`
}

type TrafficControl struct {
	Flow []Flow `xml:"flow"`
}

type Flow struct {
	Name    string `xml:"name,attr"`
	Rate    string `xml:"rate,attr"`
	Ceiling string `xml:"ceiling,attr"`
	Mode    string `xml:"mode,attr"`
	Burst   string `xml:"burst,attr"`
	Type    string `xml:"type,attr"`
	Via     string `xml:"via,attr"`
	Filter  Filter `xml:"filter"`
}

type Filter struct {
	Proto   string `xml:"proto,attr"`
	Src     string `xml:"src,attr"`
	SrcMask string `xml:"src_mask,attr"`
	SrcPort string `xml:"src_port,attr"`
	Dst     string `xml:"dst,attr"`
	DstMask string `xml:"dst_mask,attr"`
	DstPort string `xml:"dst_port,attr"`
}

func (o *Object) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	type tmp Object
	var s struct {
		tmp
		DNS             string `xml:"DNS0"`
		DNS6            string `xml:"DNS6_0"`
		ExcludeSubnets  string `xml:"ExcludeSubnets0"`
		ExcludeSubnets6 string `xml:"ExcludeSubnets6_0"`
		TrafficControl  string `xml:"TrafficControl0"`
		HDLCFraming     string `xml:"hdlc_framing"`
		DNSSuffix       string `xml:"DNSSuffix0"`
	}

	err := d.DecodeElement(&s, &start)
	if err != nil {
		return err
	}
	*o = Object(s.tmp)

	if v, err := url.QueryUnescape(s.TrafficControl); err != nil {
		return fmt.Errorf("failed to unescape %q: %s", s.TrafficControl, err)
	} else if v := strings.TrimSpace(v); v != "" {
		if err = xml.Unmarshal([]byte(v), &o.TrafficControl); err != nil {
			return err
		}
	}

	o.DNS = processIPs(s.DNS, net.IPv4len)
	o.DNS6 = processIPs(s.DNS6, net.IPv6len)
	o.ExcludeSubnets = processCIDRs(s.ExcludeSubnets, net.IPv4len)
	o.ExcludeSubnets6 = processCIDRs(s.ExcludeSubnets6, net.IPv6len)

	// TODO: support IPv6 routes
	o.Routes = inverseCIDRs4(o.ExcludeSubnets)

	o.HDLCFraming, err = strToBool(s.HDLCFraming)
	if err != nil {
		return err
	}

	if v := strings.TrimSpace(s.DNSSuffix); v != "" {
		o.DNSSuffix = strings.Split(v, ",")
	}

	return nil
}

type Session struct {
	Token         string `xml:"token"`
	Version       string `xml:"version"`
	RedirectURL   string `xml:"redirect_url"`
	MaxClientData string `xml:"max_client_data"`
}

// Profiles list
type Profiles struct {
	Type      string         `xml:"type,attr"`
	Limited   string         `xml:"limited,attr"`
	Favorites []FavoriteItem `xml:"favorite"`
}

type FavoriteItem struct {
	ID      string `xml:"id,attr"`
	Caption string `xml:"caption"`
	Name    string `xml:"name"`
	Params  string `xml:"params"`
}

type Hostname string

func (h Hostname) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	return e.EncodeElement(base64.StdEncoding.EncodeToString([]byte(h)), start)
}

func processIPs(ips string, length int) []net.IP {
	if v := strings.FieldsFunc(strings.TrimSpace(ips), util.SplitFunc); len(v) > 0 {
		var t []net.IP
		for _, v := range v {
			v := net.ParseIP(v)
			if length == net.IPv4len {
				if v.To4() != nil {
					t = append(t, v)
				}
			} else if length == net.IPv6len {
				t = append(t, v.To16())
			}
		}
		return t
	}
	return nil
}

func parseCIDRs(cidrs []string, length int) ([]*net.IPNet, error) {
	t := make([]*net.IPNet, len(cidrs))
	for i, v := range cidrs {
		var cidr *net.IPNet
		var err error

		if ip := net.ParseIP(v); ip != nil {
			cidr = &net.IPNet{
				IP:   ip,
				Mask: net.CIDRMask(32, 32),
			}
		} else {
			// parse 1.2.3.4/12 format
			_, cidr, err = net.ParseCIDR(v)
			if err != nil {
				return nil, fmt.Errorf("failed to parse %q cidr: %v", v, err)
			}
		}
		if length == net.IPv4len {
			t[i] = &net.IPNet{
				IP:   cidr.IP.To4(),
				Mask: cidr.Mask,
			}
		} else if length == net.IPv6len {
			t[i] = &net.IPNet{
				IP:   cidr.IP.To16(),
				Mask: cidr.Mask,
			}
		}
	}
	return t, nil
}

func processCIDRs(cidrs string, length int) []*net.IPNet {
	if v := strings.FieldsFunc(strings.TrimSpace(cidrs), util.SplitFunc); len(v) > 0 {
		var t []*net.IPNet
		for _, v := range v {
			// parse 1.2.3.4/255.255.255.0 format
			if v := strings.Split(v, "/"); len(v) == 2 {
				ip := net.ParseIP(v[0])
				mask := net.ParseIP(v[1])
				if ip == nil || mask == nil {
					log.Printf("Cannot parse %q CIDR", v)
					continue
				}
				if length == net.IPv4len {
					t = append(t, &net.IPNet{
						IP:   ip.To4(),
						Mask: net.IPMask(mask.To4()),
					})
				} else if length == net.IPv6len {
					t = append(t, &net.IPNet{
						IP:   ip.To16(),
						Mask: net.IPMask(mask.To16()),
					})
				}
				continue
			}
			log.Printf("Cannot parse %q CIDR", v)
		}
		return t
	}
	return nil
}

func subnetsToIPSet(subnets []*net.IPNet) *netaddr.IPSet {
	// initialize an empty IPSet
	ipSet4 := &netaddr.IPSet{}

	for _, v := range subnets {
		ipSet4.InsertNet(v)
	}

	// get a routes list
	return ipSet4
}

func inverseCIDRs4(exclude []*net.IPNet) *netaddr.IPSet {
	// initialize an empty IPSet
	ipSet4 := &netaddr.IPSet{}

	all := &net.IPNet{
		IP:   net.IPv4zero.To4(),
		Mask: net.CIDRMask(0, 32),
	}
	ipSet4.InsertNet(all)

	// remove reserved addresses (rfc8190)
	soft := &net.IPNet{
		IP:   net.IPv4zero.To4(),
		Mask: net.CIDRMask(8, 32),
	}
	ipSet4.RemoveNet(soft)

	local := &net.IPNet{
		IP:   net.IPv4(127, 0, 0, 0).To4(),
		Mask: net.CIDRMask(8, 32),
	}
	ipSet4.RemoveNet(local)

	unicast := &net.IPNet{
		IP:   net.IPv4(169, 254, 0, 0).To4(),
		Mask: net.CIDRMask(16, 32),
	}
	ipSet4.RemoveNet(unicast)

	multicast := &net.IPNet{
		IP:   net.IPv4(224, 0, 0, 0).To4(),
		Mask: net.CIDRMask(4, 32),
	}
	ipSet4.RemoveNet(multicast)

	for _, v := range exclude {
		ipSet4.RemoveNet(v)
	}

	// get a routes list
	return ipSet4
}

type AgentInfo struct {
	XMLName              xml.Name `xml:"agent_info"`
	Type                 string   `xml:"type"`
	Version              string   `xml:"version"`
	Platform             string   `xml:"platform"`
	CPU                  string   `xml:"cpu"`
	JavaScript           Bool     `xml:"javascript"`
	ActiveX              Bool     `xml:"activex"`
	Plugin               Bool     `xml:"plugin"`
	LandingURI           string   `xml:"landinguri"`
	Model                string   `xml:"model,omitempty"`
	PlatformVersion      string   `xml:"platform_version,omitempty"`
	MACAddress           string   `xml:"mac_address,omitempty"`
	UniqueID             string   `xml:"unique_id,omitempty"`
	SerialNumber         string   `xml:"serial_number,omitempty"`
	AppID                string   `xml:"app_id,omitempty"`
	AppVersion           string   `xml:"app_version,omitempty"`
	JailBreak            *Bool    `xml:"jailbreak,omitempty"`
	VPNScope             string   `xml:"vpn_scope,omitempty"`
	VPNStartType         string   `xml:"vpn_start_type,omitempty"`
	LockedMode           Bool     `xml:"lockedmode"`
	VPNTunnelType        string   `xml:"vpn_tunnel_type,omitempty"`
	Hostname             Hostname `xml:"hostname"`
	BiometricFingerprint *Bool    `xml:"biometric_fingerprint,omitempty"`
	DevicePasscodeSet    *Bool    `xml:"device_passcode_set,omitempty"`
}

type ClientData struct {
	XMLName       xml.Name `xml:"data"`
	Token         string   `xml:"token"`
	Version       string   `xml:"version"`
	RedirectURL   string   `xml:"redirect_url"`
	MaxClientData int      `xml:"max_client_data"`
}

type PreConfigProfile struct {
	XMLName   xml.Name         `xml:"PROFILE"`
	Version   string           `xml:"VERSION,attr"`
	Servers   []Server         `xml:"SERVERS>SITEM"`
	Session   preConfigSession `xml:"SESSION"`
	DNSSuffix []string         `xml:"LOCATIONS>CORPORATE>DNSSUFFIX"`
}

type Server struct {
	Address string `xml:"ADDRESS"`
	Alias   string `xml:"ALIAS"`
}

type preConfigSession struct {
	Limited              Bool           `xml:"-"`
	SaveOnExit           Bool           `xml:"-"`
	SavePasswords        Bool           `xml:"-"`
	ReuseWinlogonCreds   Bool           `xml:"-"`
	ReuseWinlogonSession Bool           `xml:"-"`
	PasswordPolicy       PasswordPolicy `xml:"PASSWORD_POLICY"`
	Update               Update         `xml:"UPDATE"`
}

func (o *preConfigSession) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	type tmp preConfigSession
	var s struct {
		tmp
		Limited              string `xml:"LIMITED,attr"`
		SaveOnExit           string `xml:"SAVEONEXIT"`
		SavePasswords        string `xml:"SAVEPASSWORDS"`
		ReuseWinlogonCreds   string `xml:"REUSEWINLOGONCREDS"`
		ReuseWinlogonSession string `xml:"REUSEWINLOGONSESSION"`
	}

	err := d.DecodeElement(&s, &start)
	if err != nil {
		return err
	}
	*o = preConfigSession(s.tmp)

	o.Limited, err = strToBool(s.Limited)
	if err != nil {
		return err
	}

	o.SaveOnExit, err = strToBool(s.SaveOnExit)
	if err != nil {
		return err
	}

	o.SavePasswords, err = strToBool(s.SavePasswords)
	if err != nil {
		return err
	}

	o.ReuseWinlogonCreds, err = strToBool(s.ReuseWinlogonCreds)
	if err != nil {
		return err
	}

	o.ReuseWinlogonSession, err = strToBool(s.ReuseWinlogonSession)
	if err != nil {
		return err
	}

	return nil
}

type PasswordPolicy struct {
	Mode    string `xml:"MODE"`
	Timeout int    `xml:"TIMEOUT"`
}

type Update struct {
	Mode Bool `xml:"-"`
}

func (o *Update) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	type tmp Update
	var s struct {
		tmp
		Mode string `xml:"MODE"`
	}

	err := d.DecodeElement(&s, &start)
	if err != nil {
		return err
	}
	*o = Update(s.tmp)

	o.Mode, err = strToBool(s.Mode)
	if err != nil {
		return err
	}

	return nil
}


================================================
FILE: pkg/config/wintun_other.go
================================================
//go:build !windows
// +build !windows

package config

func checkWinTunDriver() error {
	return nil
}


================================================
FILE: pkg/config/wintun_windows.go
================================================
//go:build windows
// +build windows

package config

import (
	"fmt"
	"os"
	"path/filepath"

	"golang.org/x/sys/windows"
)

const (
	winTun     = "wintun.dll"
	winTunSite = "https://www.wintun.net/"
)

func checkWinTunDriver() error {
	err := windows.NewLazyDLL(winTun).Load()
	if err != nil {
		dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
		if err != nil {
			dir = "gof5"
		}
		return fmt.Errorf("the %s was not found, you can download it from %s and place it into the %q directory", winTun, winTunSite, dir)
	}

	return nil
}


================================================
FILE: pkg/cookie/cookie.go
================================================
package cookie

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"syscall"

	"github.com/kayrus/gof5/pkg/config"

	"gopkg.in/yaml.v2"
)

const cookiesName = "cookies.yaml"

func parseCookies(configPath string) map[string][]string {
	cookies := make(map[string][]string)

	cookiesPath := filepath.Join(configPath, cookiesName)
	v, err := ioutil.ReadFile(cookiesPath)
	if err != nil {
		// skip "no such file or directory" error on the first startup
		if e, ok := err.(*os.PathError); !ok || e.Unwrap() != syscall.ENOENT {
			log.Printf("Cannot read cookies file: %v", err)
		}
		return cookies
	}

	if err = yaml.Unmarshal(v, &cookies); err != nil {
		log.Printf("Cannot parse cookies: %v", err)
	}

	return cookies
}

func ReadCookies(c *http.Client, u *url.URL, cfg *config.Config, sessionID string) {
	v := parseCookies(cfg.Path)
	if v, ok := v[u.Host]; ok {
		var cookies []*http.Cookie
		for _, c := range v {
			if v := strings.Split(c, "="); len(v) == 2 {
				cookies = append(cookies, &http.Cookie{Name: v[0], Value: v[1]})
			}
		}
		c.Jar.SetCookies(u, cookies)
	}

	if sessionID != "" {
		log.Printf("Overriding session ID from a CLI argument")
		// override session ID from CLI parameter
		cookies := []*http.Cookie{
			{Name: "MRHSession", Value: sessionID},
		}
		c.Jar.SetCookies(u, cookies)
	}
}

func SaveCookies(c *http.Client, u *url.URL, cfg *config.Config) error {
	raw := parseCookies(cfg.Path)
	// empty current cookies list
	raw[u.Host] = nil
	// write down new cookies
	for _, c := range c.Jar.Cookies(u) {
		raw[u.Host] = append(raw[u.Host], c.String())
	}

	cookies, err := yaml.Marshal(&raw)
	if err != nil {
		return fmt.Errorf("cannot marshal cookies: %v", err)
	}

	cookiesPath := filepath.Join(cfg.Path, cookiesName)
	if err = ioutil.WriteFile(cookiesPath, cookies, 0600); err != nil {
		return fmt.Errorf("failed to save cookies: %s", err)
	}

	if runtime.GOOS != "windows" {
		if err = os.Chown(cookiesPath, cfg.Uid, cfg.Gid); err != nil {
			return fmt.Errorf("failed to set an owner for cookies file: %s", err)
		}
	}

	return nil
}


================================================
FILE: pkg/dns/dns.go
================================================
package dns

import (
	"fmt"
	"log"
	"net"
	"strings"

	"github.com/kayrus/gof5/pkg/config"

	"github.com/miekg/dns"
)

func Start(cfg *config.Config, errChan chan error, tunDown chan struct{}) {
	dnsUDPHandler := func(w dns.ResponseWriter, m *dns.Msg) {
		dnsHandler(w, m, cfg, "udp")
	}

	dnsTCPHandler := func(w dns.ResponseWriter, m *dns.Msg) {
		dnsHandler(w, m, cfg, "tcp")
	}

	listen := net.JoinHostPort(cfg.ListenDNS.String(), "53")
	srvUDP := &dns.Server{
		Addr:    listen,
		Net:     "udp",
		Handler: dns.HandlerFunc(dnsUDPHandler),
	}
	srvTCP := &dns.Server{
		Addr:    listen,
		Net:     "tcp",
		Handler: dns.HandlerFunc(dnsTCPHandler),
	}

	go func() {
		if err := srvUDP.ListenAndServe(); err != nil {
			errChan <- fmt.Errorf("failed to set udp listener: %v", err)
			return
		}
	}()
	go func() {
		if err := srvTCP.ListenAndServe(); err != nil {
			errChan <- fmt.Errorf("failed to set tcp listener: %v", err)
			return
		}
	}()

	go func() {
		<-tunDown
		log.Printf("Shutting down DNS proxy")
		srvUDP.Shutdown()
		srvTCP.Shutdown()
	}()
}

func dnsHandler(w dns.ResponseWriter, m *dns.Msg, cfg *config.Config, proto string) {
	c := new(dns.Client)
	for _, suffix := range cfg.DNS {
		if strings.HasSuffix(m.Question[0].Name, suffix) {
			if cfg.Debug {
				log.Printf("Resolving %q using VPN DNS", m.Question[0].Name)
			}
			for _, s := range cfg.F5Config.Object.DNS {
				if err := handleCustom(w, m, c, s); err == nil {
					return
				}
			}
		}
	}
	for _, s := range cfg.DNSServers {
		if err := handleCustom(w, m, c, s); err == nil {
			return
		}
	}
}

func handleCustom(w dns.ResponseWriter, o *dns.Msg, c *dns.Client, ip net.IP) error {
	m := new(dns.Msg)
	o.CopyTo(m)
	r, _, err := c.Exchange(m, net.JoinHostPort(ip.String(), "53"))
	if r == nil || err != nil {
		return fmt.Errorf("failed to resolve %q", m.Question[0].Name)
	}
	w.WriteMsg(r)
	return nil
}


================================================
FILE: pkg/link/cmd_nix.go
================================================
//go:build !windows
// +build !windows

package link

import (
	"log"
	"os/exec"
	"runtime"
	"syscall"

	"github.com/kayrus/gof5/pkg/config"
)

func Cmd(cfg *config.Config) *exec.Cmd {
	var cmd *exec.Cmd
	if cfg.Driver == "pppd" {
		// VPN
		if cfg.IPv6 && bool(cfg.F5Config.Object.IPv6) {
			cfg.PPPdArgs = append(cfg.PPPdArgs,
				"ipv6cp-accept-local",
				"ipv6cp-accept-remote",
				"+ipv6",
			)
		} else {
			cfg.PPPdArgs = append(cfg.PPPdArgs,
				// TODO: clarify why it doesn't work
				"noipv6", // Unsupported protocol 'IPv6 Control Protocol' (0x8057) received
			)
		}
		if cfg.Debug {
			cfg.PPPdArgs = append(cfg.PPPdArgs,
				"debug",
				"kdebug", "1",
			)
			log.Printf("pppd args: %q", cfg.PPPdArgs)
		}

		switch runtime.GOOS {
		default:
			cmd = exec.Command("pppd", cfg.PPPdArgs...)
		case "freebsd":
			cmd = exec.Command("ppp", "-direct")
		}

		// don't forward parent process signals to a child process
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Setpgid: true,
			Pgid:    0,
		}
		return cmd
	}
	return nil
}


================================================
FILE: pkg/link/cmd_windows.go
================================================
//go:build windows
// +build windows

package link

import (
	"os/exec"

	"github.com/kayrus/gof5/pkg/config"
)

func Cmd(_ *config.Config) *exec.Cmd {
	return nil
}


================================================
FILE: pkg/link/f5.go
================================================
package link

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"net"

	"golang.org/x/net/ipv4"
	"golang.org/x/net/ipv6"
)

func readBuf(buf, sep []byte) []byte {
	n := bytes.Index(buf, sep)
	if n == 0 {
		return buf[len(sep):]
	}
	return nil
}

var (
	ppp       = []byte{0xff, 0x03}
	pppLCP    = []byte{0xc0, 0x21}
	pppIPCP   = []byte{0x80, 0x21}
	pppIPv6CP = []byte{0x80, 0x57}
	// LCP auth
	mtuRequest = []byte{0x00, 0x18}
	// Link-Discriminator
	terminate = []byte{0x00, 0x17}
	// No network protocols
	noProtocols = []byte{0x00, 0x20}
	// Session-Timeout
	timeout = []byte{0x00, 0x13}
	//
	mtuResponse = []byte{0x00, 0x12}
	protoRej    = []byte{0x00, 0x2c}
	mtuHeader   = []byte{0x01, 0x04}
	mtuSize     = 2
	ipv6type    = []byte{0x00, 0x0e}
	ipv4type    = []byte{0x00, 0x0a}
	v4          = []byte{0x06}
	v6          = []byte{0x0a}
	pfc         = []byte{0x07, 0x02}
	acfc        = []byte{0x08, 0x02}
	accm        = []byte{0x02, 0x06, 0x00, 0x00, 0x00, 0x00}
	magicHeader = []byte{0x05, 0x06}
	magicSize   = 4
	ipv4header  = []byte{0x21}
	ipv6header  = []byte{0x57}
	//
	confRequest = []byte{0x01}
	confAck     = []byte{0x02}
	confNack    = []byte{0x03}
	confRej     = []byte{0x04}
	confTermReq = []byte{0x05}
	protoReject = []byte{0x08}
	echoReq     = []byte{0x09}
	echoRep     = []byte{0x0a}
)

func bytesToIPv4(bytes []byte) net.IP {
	return net.IP(append(bytes[:0:0], bytes...))
}

func bytesToIPv6(bytes []byte) net.IP {
	return net.IP(append([]byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, append(bytes[:0:0], bytes...)...))
}

func processPPP(l *vpnLink, buf []byte, dstBuf *bytes.Buffer) error {
	// process ipv4 traffic
	if v := readBuf(buf, ipv4header); v != nil {
		if l.debug {
			log.Printf("Read parsed ipv4 %d bytes from http:\n%s", len(v), hex.Dump(v))
			header, _ := ipv4.ParseHeader(v)
			log.Printf("ipv4 from http: %s", header)
		}

		wn, err := l.iface.Write(v)
		if err != nil {
			return fmt.Errorf("fatal write to tun: %s", err)
		}
		if l.debug {
			log.Printf("Sent %d bytes to tun", wn)
		}
		return nil
	}

	// process ipv6 traffic
	if v := readBuf(buf, ipv6header); v != nil {
		if l.debug {
			log.Printf("Read parsed ipv6 %d bytes from http:\n%s", len(v), hex.Dump(v))
			header, _ := ipv6.ParseHeader(v)
			log.Printf("ipv6 from http: %s", header)
		}

		wn, err := l.iface.Write(v)
		if err != nil {
			return fmt.Errorf("fatal write to tun: %s", err)
		}
		if l.debug {
			log.Printf("Sent %d bytes to tun", wn)
		}
		return nil
	}

	// TODO: support IPv4 only
	if v := readBuf(buf, pppIPCP); v != nil {
		if v := readBuf(v, confRequest); v != nil {
			id := v[0]
			if v := readBuf(v[1:], ipv4type); v != nil {
				id2 := v[0]
				if v := readBuf(v[1:], v4); v != nil {
					l.serverIPv4 = bytesToIPv4(v)
					log.Printf("id: %d, id2: %d, Remote IPv4 requested: %s", id, id2, l.serverIPv4)

					doResp := &bytes.Buffer{}
					doResp.Write(ppp)
					doResp.Write(pppIPCP)
					//
					doResp.Write(confAck)
					doResp.WriteByte(id)
					doResp.Write(ipv4type)
					doResp.WriteByte(id2)
					doResp.Write(v4)
					doResp.Write(v)

					err := toF5(l, doResp.Bytes(), dstBuf)
					if err != nil {
						return err
					}

					doResp = &bytes.Buffer{}
					doResp.Write(ppp)
					doResp.Write(pppIPCP)
					//
					doResp.Write(confRequest)
					doResp.WriteByte(id)
					doResp.Write(ipv4type)
					doResp.WriteByte(id2)
					doResp.Write(v4)
					for i := 0; i < 4; i++ {
						doResp.WriteByte(0)
					}

					return toF5(l, doResp.Bytes(), dstBuf)
				}
			}
		}
		if v := readBuf(v, confAck); v != nil {
			id := v[0]
			if v := readBuf(v[1:], ipv4type); v != nil {
				id2 := v[0]
				if v := readBuf(v[1:], v4); v != nil {
					l.localIPv4 = bytesToIPv4(v)
					log.Printf("id: %d, id2: %d, Local IPv4 acknowledged: %s", id, id2, l.localIPv4)

					// connection established
					close(l.pppUp)

					return nil
				}
			}
		}
		if v := readBuf(v, confNack); v != nil {
			id := v[0]
			if v := readBuf(v[1:], ipv4type); v != nil {
				id2 := v[0]
				if v := readBuf(v[1:], v4); v != nil {
					log.Printf("id: %d, id2: %d, Local IPv4 not acknowledged: %s", id, id2, bytesToIPv4(v))

					doResp := &bytes.Buffer{}
					doResp.Write(ppp)
					doResp.Write(pppIPCP)
					//
					doResp.Write(confRequest)
					doResp.WriteByte(id)
					doResp.Write(ipv4type)
					doResp.WriteByte(id2)
					doResp.Write(v4)
					doResp.Write(v)

					return toF5(l, doResp.Bytes(), dstBuf)
				}
			}
		}
	}

	// pppIPv6CP
	if v := readBuf(buf, pppIPv6CP); v != nil {
		if v := readBuf(v, confRequest); v != nil {
			id := v[0]
			if v := readBuf(v[1:], ipv6type); v != nil {
				id2 := v[0]
				if v := readBuf(v[1:], v6); v != nil {
					l.serverIPv6 = bytesToIPv6(v)
					log.Printf("id: %d, id2: %d, Remote IPv6 requested: %s", id, id2, l.serverIPv6)

					doResp := &bytes.Buffer{}
					doResp.Write(ppp)
					doResp.Write(pppIPv6CP)
					//
					doResp.Write(confAck)
					doResp.WriteByte(id)
					doResp.Write(ipv6type)
					doResp.WriteByte(id2)
					doResp.Write(v6)
					doResp.Write(v)

					err := toF5(l, doResp.Bytes(), dstBuf)
					if err != nil {
						return err
					}

					doResp = &bytes.Buffer{}
					doResp.Write(ppp)
					doResp.Write(pppIPv6CP)
					//
					doResp.Write(confRequest)
					doResp.WriteByte(id)
					doResp.Write(ipv6type)
					doResp.WriteByte(id2)
					doResp.Write(v6)
					for i := 0; i < 8; i++ {
						doResp.WriteByte(0)
					}

					return toF5(l, doResp.Bytes(), dstBuf)
				}
			}
		}
		if v := readBuf(v, confAck); v != nil {
			id := v[0]
			if v := readBuf(v[1:], ipv6type); v != nil {
				id2 := v[0]
				if v := readBuf(v[1:], v6); v != nil {
					l.localIPv6 = bytesToIPv6(v)
					log.Printf("id: %d, id2: %d, Local IPv6 acknowledged: %s", id, id2, l.localIPv6)

					return nil
				}
			}
		}
		if v := readBuf(v, confNack); v != nil {
			id := v[0]
			if v := readBuf(v[1:], ipv6type); v != nil {
				id2 := v[0]
				if v := readBuf(v[1:], v6); v != nil {
					log.Printf("id: %d, id2: %d, Local IPv6 not acknowledged: %s", id, id2, bytesToIPv6(v))

					doResp := &bytes.Buffer{}
					doResp.Write(ppp)
					doResp.Write(pppIPv6CP)
					//
					doResp.Write(confRequest)
					doResp.WriteByte(id)
					doResp.Write(ipv6type)
					doResp.WriteByte(id2)
					doResp.Write(v6)
					doResp.Write(v)

					return toF5(l, doResp.Bytes(), dstBuf)
				}
			}
		}
	}

	// it is PPP header
	if v := readBuf(buf, ppp); v != nil {
		// it is pppLCP
		if v := readBuf(v, pppLCP); v != nil {
			if v := readBuf(v, confTermReq); v != nil {
				id := v[0]
				if v := readBuf(v[1:], terminate); v != nil {
					return fmt.Errorf("id: %d, Link terminated with: %s", id, v)
				}
				if v := readBuf(v[1:], timeout); v != nil {
					return fmt.Errorf("id: %d, Link timed out with: %s", id, v)
				}
				if v := readBuf(v[1:], noProtocols); v != nil {
					return fmt.Errorf("id: %d, Link terminated with: %s", id, v)
				}
			}
			if v := readBuf(v, echoReq); v != nil {
				id := v[0]
				if l.debug {
					log.Printf("id: %d, echo", id)
				}
				// live pings
				doResp := &bytes.Buffer{}
				doResp.Write(ppp)
				doResp.Write(pppLCP)
				//
				doResp.Write(echoRep)
				doResp.WriteByte(id)
				doResp.Write(v[1:])

				return toF5(l, doResp.Bytes(), dstBuf)
			}
			if v := readBuf(v, protoReject); v != nil {
				id := v[0]
				if v := readBuf(v[1:], protoRej); v != nil {
					log.Printf("id: %d, Protocol reject:\n%s", id, hex.Dump(v))
					return nil
				}
			}
			// it is pppLCP
			if v := readBuf(v, confRequest); v != nil {
				id := v[0]
				// configuration requested
				if v := readBuf(v[1:], mtuRequest); v != nil {
					// MTU request
					if v := readBuf(v, mtuHeader); v != nil {
						// set MTU
						t := v[:mtuSize]
						l.mtu = append(t[:0:0], t...)
						l.mtuInt = binary.BigEndian.Uint16(l.mtu)
						log.Printf("MTU: %d", l.mtuInt)
						if v := readBuf(v[mtuSize:], accm); v != nil {
							if v := readBuf(v, magicHeader); v != nil {
								magic := v[:magicSize]
								log.Printf("Magic: %x", magic)
								log.Printf("PFC: %x", v[magicSize:magicSize+len(pfc)])
								log.Printf("ACFC: %x", v[magicSize+len(pfc):])

								doResp := &bytes.Buffer{}
								doResp.Write(ppp)
								doResp.Write(pppLCP)
								//
								doResp.Write(confRequest)
								doResp.WriteByte(id)
								doResp.Write(ipv6type)
								doResp.Write(accm)
								doResp.Write(pfc)
								doResp.Write(acfc)

								err := toF5(l, doResp.Bytes(), dstBuf)
								if err != nil {
									return err
								}

								doResp = &bytes.Buffer{}
								doResp.Write(ppp)
								doResp.Write(pppLCP)
								//
								doResp.Write(confRej)
								//doResp.Write(confRequest)
								doResp.WriteByte(id)
								doResp.Write(ipv4type)
								doResp.Write(magicHeader)
								doResp.Write(magic)

								return toF5(l, doResp.Bytes(), dstBuf)
							}
							return fmt.Errorf("wrong magic header: %x", v)
						}
						return fmt.Errorf("wrong ACCM: %x", v)
					}
				}
				if v := readBuf(v[1:], mtuResponse); v != nil {
					if v := readBuf(v, mtuHeader); v != nil {
						if v := readBuf(v, l.mtu); v != nil {
							if v := readBuf(v, accm); v != nil {
								if v := readBuf(v, pfc); v != nil {
									if v := readBuf(v, acfc); v != nil {
										log.Printf("id: %d, MTU accepted", id)

										doResp := &bytes.Buffer{}
										doResp.Write(ppp)
										doResp.Write(pppLCP)
										//
										doResp.Write(confAck)
										doResp.WriteByte(id)
										doResp.Write(mtuResponse)
										doResp.Write(mtuHeader)
										doResp.Write(l.mtu)
										doResp.WriteByte(id)
										doResp.Write(v4)
										for i := 0; i < 4; i++ {
											doResp.WriteByte(0)
										}
										doResp.Write(pfc)
										doResp.Write(acfc)

										return toF5(l, doResp.Bytes(), dstBuf)
									}
								}
							}
						}
					}
				}
			}
			// do set
			if v := readBuf(v, confAck); v != nil {
				// required settings
				id := v[0]
				if v := readBuf(v[1:], ipv6type); v != nil {
					if v := readBuf(v, accm); v != nil {
						if v := readBuf(v, pfc); v != nil {
							if v := readBuf(v, acfc); v != nil {
								log.Printf("id: %d, IPV6 accepted", id)
								return nil
							}
						}
					}
				}
			}
			if v := readBuf(v, confNack); v != nil {
				id := v[0]
				if v := readBuf(v[1:], mtuRequest); v != nil {
					if v := readBuf(v, mtuHeader); v != nil {
						if v := readBuf(v, l.mtu); v != nil {
							return fmt.Errorf("id: %d, MTU not acknowledged:\n%s", id, hex.Dump(v))
						}
					}
				}
				if v := readBuf(v[1:], ipv4type); v != nil {
					if v := readBuf(v, magicHeader); v != nil {
						return fmt.Errorf("id: %d, IPv4 not acknowledged:\n%s", id, hex.Dump(v))
					}
				}
			}
		}
	}

	return fmt.Errorf("unknown PPP data:\n%s", hex.Dump(buf))
}

func fromF5(l *vpnLink, dstBuf *bytes.Buffer) error {
	// read the F5 packet header
	buf := make([]byte, 2)
	_, err := io.ReadFull(l.HTTPConn, buf)
	if err != nil {
		return fmt.Errorf("failed to read F5 packet header: %s", err)
	}
	if !(buf[0] == 0xf5 && buf[1] == 00) {
		return fmt.Errorf("incorrect F5 header: %x", buf)
	}

	// read the F5 packet size
	var pkglen uint16
	err = binary.Read(l.HTTPConn, binary.BigEndian, &pkglen)
	if err != nil {
		return fmt.Errorf("failed to read F5 packet size: %s", err)
	}

	// read the packet
	buf = make([]byte, pkglen)
	n, err := io.ReadFull(l.HTTPConn, buf)
	if err != nil {
		return fmt.Errorf("failed to read F5 packet of the %d size: %s", pkglen, err)
	}
	if n != int(pkglen) {
		return fmt.Errorf("incorrect F5 packet size: %d, expected: %d", n, pkglen)
	}

	// process the packet
	return processPPP(l, buf, dstBuf)
}

// Decode F5 packet
// http->tun
func (l *vpnLink) HttpToTun() {
	dstBuf := &bytes.Buffer{}
	for {
		select {
		case <-l.TunDown:
			return
		default:
			err := fromF5(l, dstBuf)
			if err != nil {
				l.ErrChan <- err
				return
			}
		}
	}
}

func toF5(l *vpnLink, buf []byte, dst *bytes.Buffer) error {
	// TODO: move buffer initialization into tunToHTTP
	// probably a buffered pipe would be nicer
	length := len(buf)
	if length == 0 {
		return fmt.Errorf("cannot encapsulate zero packet")
	}

	defer dst.Reset()

	// TODO: check packet header length (ipv4.HeaderLen, ipv6.HeaderLen)
	switch buf[0] >> 4 {
	case ipv4.Version:
		length += len(ipv4header)
	case ipv6.Version:
		length += len(ipv6header)
	}

	_, err := dst.Write([]byte{0xf5, 0x00})
	if err != nil {
		return fmt.Errorf("failed to write F5 header: %s", err)
	}
	err = binary.Write(dst, binary.BigEndian, uint16(length))
	if err != nil {
		return fmt.Errorf("failed to write F5 header size: %s", err)
	}

	switch buf[0] >> 4 {
	case ipv4.Version:
		_, err = dst.Write(ipv4header)
	case ipv6.Version:
		_, err = dst.Write(ipv6header)
	}
	if err != nil {
		return fmt.Errorf("failed to write IP header: %s", err)
	}

	if l.debug {
		log.Printf("Sending from pppd:\n%s", hex.Dump(buf))
	}

	_, err = dst.Write(buf)
	if err != nil {
		return fmt.Errorf("fatal write to http: %s", err)
	}
	wn, err := io.Copy(l.HTTPConn, dst)
	if err != nil {
		return fmt.Errorf("fatal write to http: %s", err)
	}
	if l.debug {
		log.Printf("Sent %d bytes to http", wn)
	}

	return nil
}

// Encode into F5 packet
// tun->http
func (l *vpnLink) TunToHTTP() {
	buf := make([]byte, bufferSize)
	dstBuf := &bytes.Buffer{}
	for {
		select {
		case <-l.TunDown:
			return
		case <-l.tunUp:
			rn, err := l.iface.Read(buf)
			if err != nil {
				if err != io.EOF {
					l.ErrChan <- fmt.Errorf("fatal read tun: %s", err)
				}
				return
			}
			if l.debug {
				log.Printf("Read %d bytes from tun:\n%s", rn, hex.Dump(buf[:rn]))
				header, _ := ipv4.ParseHeader(buf[:rn])
				log.Printf("ipv4 from tun: %s", header)
			}

			err = toF5(l, buf[:rn], dstBuf)
			if err != nil {
				l.ErrChan <- err
				return
			}
		}
	}
}


================================================
FILE: pkg/link/link.go
================================================
package link

import (
	"bufio"
	"crypto/tls"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"net/http"
	"runtime"
	"sync"
	"time"

	"github.com/kayrus/gof5/pkg/config"
	"github.com/kayrus/gof5/pkg/dns"

	"github.com/fatih/color"
	"github.com/kayrus/tuncfg/resolv"
	"github.com/kayrus/tuncfg/route"
	"github.com/kayrus/tuncfg/tun"
	"github.com/pion/dtls/v2"
)

const (
	// TUN MTU should not be bigger than buffer size
	bufferSize   = 1500
	userAgentVPN = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0; F5 Networks Client)"
)

var colorlog = log.New(color.Error, "", log.LstdFlags)

type vpnLink struct {
	sync.Mutex
	HTTPConn    io.ReadWriteCloser
	ErrChan     chan error
	TunDown     chan struct{}
	PppdErrChan chan error
	iface       io.ReadWriteCloser
	name        string
	// pppUp is used to wait for the PPP handshake (wireguard only)
	pppUp chan struct{}
	// tunUp is used to wait for the TUN interface (wireguard and pppd)
	tunUp         chan struct{}
	serverIPs     []net.IP
	localIPv4     net.IP
	serverIPv4    net.IP
	localIPv6     net.IP
	serverIPv6    net.IP
	mtu           []byte
	mtuInt        uint16
	debug         bool
	routeHandler  *route.Handler
	resolvHandler *resolv.Handler
}

func randomHostname(n int) []byte {
	var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

	rand.Seed(time.Now().UnixNano())

	b := make([]byte, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return b
}

// init a TLS connection
func InitConnection(server string, cfg *config.Config, tlsConfig *tls.Config) (*vpnLink, error) {
	getURL := fmt.Sprintf("https://%s/myvpn?sess=%s&hostname=%s&hdlc_framing=%s&ipv4=%s&ipv6=%s&Z=%s",
		server,
		cfg.F5Config.Object.SessionID,
		base64.StdEncoding.EncodeToString(randomHostname(8)),
		config.Bool(cfg.Driver == "pppd"),
		cfg.F5Config.Object.IPv4,
		config.Bool(cfg.IPv6 && bool(cfg.F5Config.Object.IPv6)),
		cfg.F5Config.Object.UrZ,
	)

	serverIPs, err := net.LookupIP(server)
	if err != nil || len(serverIPs) == 0 {
		return nil, fmt.Errorf("failed to resolve %s: %s", server, err)
	}

	// define link channels
	l := &vpnLink{
		ErrChan:     make(chan error, 1),
		TunDown:     make(chan struct{}, 1),
		PppdErrChan: make(chan error, 1),
		serverIPs:   serverIPs,
		pppUp:       make(chan struct{}, 1),
		tunUp:       make(chan struct{}, 1),
		debug:       cfg.Debug,
	}

	if cfg.DTLS && cfg.F5Config.Object.TunnelDTLS {
		s := fmt.Sprintf("%s:%s", server, cfg.F5Config.Object.TunnelPortDTLS)
		log.Printf("Connecting to %s using DTLS", s)
		addr, err := net.ResolveUDPAddr("udp", s)
		if err != nil {
			return nil, fmt.Errorf("failed to resolve UDP address: %s", err)
		}
		conf := &dtls.Config{
			RootCAs:            tlsConfig.RootCAs,
			Certificates:       tlsConfig.Certificates,
			InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
			ServerName:         server,
		}
		l.HTTPConn, err = dtls.Dial("udp", addr, conf)
		if err != nil {
			return nil, fmt.Errorf("failed to dial %s:%s: %s", server, cfg.F5Config.Object.TunnelPortDTLS, err)
		}
	} else {
		l.HTTPConn, err = tls.Dial("tcp", fmt.Sprintf("%s:443", server), tlsConfig)
		if err != nil {
			return nil, fmt.Errorf("failed to dial %s:443: %s", server, err)
		}
	}

	req, err := http.NewRequest("GET", getURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create VPN session request: %s", err)
	}
	req.Header.Set("User-Agent", userAgentVPN)
	err = req.Write(l.HTTPConn)
	if err != nil {
		return nil, fmt.Errorf("failed to send VPN session request: %s", err)
	}

	if l.debug {
		log.Printf("URL: %s", getURL)
	}

	resp, err := http.ReadResponse(bufio.NewReader(l.HTTPConn), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get initial VPN connection response: %s", err)
	}
	resp.Body.Close()

	l.localIPv4 = net.ParseIP(resp.Header.Get("X-VPN-client-IP"))
	l.serverIPv4 = net.ParseIP(resp.Header.Get("X-VPN-server-IP"))
	l.localIPv6 = net.ParseIP(resp.Header.Get("X-VPN-client-IPv6"))
	l.serverIPv6 = net.ParseIP(resp.Header.Get("X-VPN-server-IPv6"))

	if l.debug {
		log.Printf("Client IP: %s", l.localIPv4)
		log.Printf("Server IP: %s", l.serverIPv4)
		if l.localIPv6 != nil {
			log.Printf("Client IPv6: %s", l.localIPv6)
		}
		if l.localIPv6 != nil {
			log.Printf("Server IPv6: %s", l.serverIPv6)
		}
	}

	return l, nil
}

func (l *vpnLink) createTunDevice() error {
	if l.mtuInt+tun.Offset > bufferSize {
		return fmt.Errorf("MTU exceeds the %d buffer limit", bufferSize)
	}

	log.Printf("Using wireguard module to create tunnel")
	ifname := ""
	switch runtime.GOOS {
	case "darwin":
		ifname = "utun"
	case "windows":
		ifname = "gof5"
	}

	local := &net.IPNet{
		IP:   l.localIPv4,
		Mask: net.CIDRMask(32, 32),
	}
	gw := &net.IPNet{
		IP:   l.serverIPv4,
		Mask: net.CIDRMask(32, 32),
	}
	tunDev, err := tun.OpenTunDevice(local, gw, ifname, int(l.mtuInt))
	if err != nil {
		return fmt.Errorf("failed to create an interface: %s", err)
	}
	l.name, err = tunDev.Name()
	if err != nil {
		if e := tunDev.Close(); e != nil {
			log.Printf("error closing interface: %v", e)
		}
		return fmt.Errorf("failed to get an interface name: %s", err)
	}

	log.Printf("Created %s interface", l.name)
	l.iface = &tun.Tunnel{NativeTun: tunDev}

	// can now process the traffic
	close(l.tunUp)

	return nil
}

func (l *vpnLink) configureDNS(cfg *config.Config) error {
	var err error
	// this is used only in linux/freebsd to store /etc/resolv.conf backup
	resolv.AppName = "gof5"

	dnsSuffixes := cfg.F5Config.Object.DNSSuffix
	var dnsServers []net.IP
	if len(cfg.DNS) == 0 {
		// route everything through VPN gatewy
		dnsServers = cfg.F5Config.Object.DNS
	} else {
		// route only configured suffixes via local DNS proxy
		dnsServers = []net.IP{cfg.ListenDNS}
	}

	// define DNS servers, provided by F5
	l.resolvHandler, err = resolv.New(l.name, dnsServers, dnsSuffixes, cfg.RewriteResolv)
	if err != nil {
		return err
	}

	if cfg.DisableDNS {
		// TODO: this is a hack to get real DNS servers, need to be fixed in "tuncfg"
		l.resolvHandler.IsResolve()
		// no further configuration is required
		// get current DNS setting and exit
		return nil
	}

	if len(cfg.DNS) > 0 && !l.resolvHandler.IsResolve() {
		// combine local network search with VPN gateway search
		dnsSuffixes = l.resolvHandler.GetOriginalSuffixes()
		existingSuffixes := make(map[string]bool)
		for _, existingSuffix := range dnsSuffixes {
			existingSuffixes[existingSuffix] = true
		}

		for _, newSuffix := range cfg.F5Config.Object.DNSSuffix {
			if !existingSuffixes[newSuffix] {
				dnsSuffixes = append(dnsSuffixes, newSuffix)
			}
		}
		l.resolvHandler.SetSuffixes(dnsSuffixes)
	}

	if l.resolvHandler.IsResolve() {
		// resolve daemon will route necessary domains through VPN gatewy
		log.Printf("Detected systemd-resolved")
		l.resolvHandler.SetDNSServers(cfg.F5Config.Object.DNS)
		if len(cfg.DNS) > 0 {
			log.Printf("Forwarding %q DNS requests to %q", cfg.DNS, cfg.F5Config.Object.DNS)
			l.resolvHandler.SetDNSDomains(cfg.DNS)
			log.Printf("Default DNS servers: %q", l.resolvHandler.GetOriginalDNS())
		} else {
			// route all DNS queries via VPN
			log.Printf("Forwarding all DNS requests to %q", cfg.F5Config.Object.DNS)
			l.resolvHandler.SetDNSDomains([]string{"."})
		}
	}

	// set DNS and additionally detect original DNS servers, e.g. when NetworkManager is used
	err = l.resolvHandler.Set()
	if err != nil {
		return err
	}

	if !l.resolvHandler.IsResolve() {
		if len(cfg.DNS) == 0 {
			log.Printf("Forwarding all DNS requests to %q", cfg.F5Config.Object.DNS)
			return nil
		}
		cfg.DNSServers = l.resolvHandler.GetOriginalDNS()
		log.Printf("Serving DNS proxy on %s:53", cfg.ListenDNS)
		log.Printf("Forwarding %q DNS requests to %q", cfg.DNS, cfg.F5Config.Object.DNS)
		log.Printf("Default DNS servers: %q", cfg.DNSServers)
		dns.Start(cfg, l.ErrChan, l.TunDown)
	}

	return nil
}

// wait for pppd and config DNS and routes
func (l *vpnLink) WaitAndConfig(cfg *config.Config) {
	// wait for ppp handshake completed
	<-l.pppUp

	l.Lock()
	defer l.Unlock()

	var err error

	if cfg.Driver != "pppd" {
		// create TUN
		err = l.createTunDevice()
		if err != nil {
			l.ErrChan <- err
			return
		}
		defer func() {
			if err != nil && l.iface != nil {
				// destroy interface on error
				if e := l.iface.Close(); e != nil {
					log.Printf("error closing interface: %v", e)
				}
			}
		}()
	}

	err = l.configureDNS(cfg)
	if err != nil {
		l.ErrChan <- err
		return
	}

	// set routes
	log.Printf("Setting routes on %s interface", l.name)

	// set custom routes
	routes := cfg.Routes
	if routes == nil {
		log.Printf("Applying routes, pushed from F5 VPN server")
		routes = cfg.F5Config.Object.Routes
	}

	// exclude F5 gateway IPs
	for _, dst := range l.serverIPs {
		// exclude only ipv4
		if v := dst.To4(); v != nil {
			local := &net.IPNet{
				IP:   v,
				Mask: net.CIDRMask(32, 32),
			}
			routes.RemoveNet(local)
		}
	}

	// exclude local DNS servers, when they are not located inside the LAN
	for _, v := range l.resolvHandler.GetOriginalDNS() {
		localDNS := &net.IPNet{
			IP:   v,
			Mask: net.CIDRMask(32, 32),
		}
		routes.RemoveNet(localDNS)
	}

	var gw net.IP
	if runtime.GOOS == "windows" {
		// windows requires both gateway and interface name
		gw = l.serverIPv4
	}

	l.routeHandler, err = route.New(l.name, routes.GetNetworks(), gw, 0)
	if err != nil {
		l.ErrChan <- err
		return
	}
	l.routeHandler.Add()

	colorlog.Print(color.HiGreenString("Connection established"))
}

// restore config
func (l *vpnLink) RestoreConfig(cfg *config.Config) {
	l.Lock()
	defer l.Unlock()

	if l.routeHandler != nil {
		log.Printf("Removing routes from %s interface", l.name)
		l.routeHandler.Del()
	}

	if !cfg.DisableDNS {
		if l.resolvHandler != nil {
			log.Printf("Restoring DNS settings")
			l.resolvHandler.Restore()
		}
	}

	if cfg.Driver != "pppd" {
		if l.iface != nil {
			err := l.iface.Close()
			if err != nil {
				log.Printf("error closing interface: %v", err)
			}
		}
	}
}


================================================
FILE: pkg/link/pppd.go
================================================
package link

import (
	"bufio"
	"bytes"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"os/exec"
	"strings"
	"syscall"

	"github.com/kayrus/gof5/pkg/util"

	"github.com/fatih/color"
	"github.com/hpcloud/tail"
	"github.com/zaninime/go-hdlc"
	"golang.org/x/net/ipv4"
)

// TODO: handle "fatal read pppd: read /dev/ptmx: input/output error"
// TODO: speed test vs native

func (l *vpnLink) decodeHDLC(buf []byte, src string) {
	tmp := bytes.NewBuffer(buf)
	frame, err := hdlc.NewDecoder(tmp).ReadFrame()
	if err != nil {
		log.Printf("fatal decode HDLC frame from %s: %s", src, err)
		return
		/*
			l.ErrChan <- fmt.Errorf("fatal decode HDLC frame from %s: %s", source, err)
			return
		*/
	}
	log.Printf("Decoded %t prefix HDLC frame from %s:\n%s", frame.HasAddressCtrlPrefix, src, hex.Dump(frame.Payload))
	h, err := ipv4.ParseHeader(frame.Payload[:])
	if err != nil {
		log.Printf("fatal to parse TCP header from %s: %s", src, err)
		return
		/*
			l.ErrChan <- fmt.Errorf("fatal to parse TCP header: %s", err)
			return
		*/
	}
	log.Printf("TCP: %s", h)
}

// http->tun
func (l *vpnLink) PppdHTTPToTun(pppd io.WriteCloser) {
	buf := make([]byte, bufferSize)
	for {
		select {
		case <-l.TunDown:
			return
		default:
			rn, err := l.HTTPConn.Read(buf)
			if err != nil {
				if err != io.EOF {
					l.ErrChan <- fmt.Errorf("fatal read http: %s", err)
				}
				return
			}
			if l.debug {
				l.decodeHDLC(buf[:rn], "http")
				log.Printf("Read %d bytes from http:\n%s", rn, hex.Dump(buf[:rn]))
			}
			wn, err := pppd.Write(buf[:rn])
			if err != nil {
				l.ErrChan <- fmt.Errorf("fatal write to pppd: %s", err)
				return
			}
			if l.debug {
				log.Printf("Sent %d bytes to pppd", wn)
			}
		}
	}
}

// tun->http
func (l *vpnLink) PppdTunToHTTP(pppd io.ReadCloser) {
	buf := make([]byte, bufferSize)
	for {
		select {
		case <-l.TunDown:
			return
		default:
			rn, err := pppd.Read(buf)
			if err != nil {
				if err != io.EOF {
					l.ErrChan <- fmt.Errorf("fatal read pppd: %s", err)
				}
				return
			}
			if l.debug {
				log.Printf("Read %d bytes from pppd:\n%s", rn, hex.Dump(buf[:rn]))
				l.decodeHDLC(buf[:rn], "pppd")
			}
			wn, err := l.HTTPConn.Write(buf[:rn])
			if err != nil {
				l.ErrChan <- fmt.Errorf("fatal write to http: %s", err)
				return
			}
			if l.debug {
				log.Printf("Sent %d bytes to http", wn)
			}
		}
	}
}

// monitor the the ppp/pppd child process status
func (l *vpnLink) CatchPPPDTermination(cmd *exec.Cmd) {
	defer close(l.PppdErrChan)
	if err := cmd.Wait(); err != nil {
		l.PppdErrChan <- fmt.Errorf("%s process %v", cmd.Path, err)
		return
	}
}

// gracefully stop the ppp/pppd child
func (l *vpnLink) StopPPPDChild(cmd *exec.Cmd) {
	if cmd != nil && cmd.Process != nil {
		cmd.Process.Signal(syscall.SIGTERM)
		<-l.PppdErrChan
	}
}

// pppd log parser
func (l *vpnLink) PppdLogParser(stderr io.Reader) {
	scanner := bufio.NewScanner(stderr)
	for scanner.Scan() {
		str := scanner.Text()
		if strings.Contains(str, "Using interface") {
			if v := strings.FieldsFunc(str, util.SplitFunc); len(v) > 0 {
				l.name = v[len(v)-1]
			}
		}
		if strings.Contains(str, "remote IP address") {
			close(l.pppUp)
		}
		colorlog.Print(color.HiGreenString(str))
	}
}

// freebsd ppp log parser
// TODO: talk directly via pppctl
// /etc/ppp/ppp.conf should have `set server /var/run/ppp "" 0177`
func (l *vpnLink) PppLogParser() {
	t, err := tail.TailFile("/var/log/ppp.log", tail.Config{
		Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
		Follow:   true,
		Logger:   tail.DiscardingLogger,
	})
	if err != nil {
		l.ErrChan <- fmt.Errorf("failed to read ppp log: %s", err)
		return
	}
	for line := range t.Lines {
		str := line.Text
		// strip syslog prefix
		if v := strings.SplitN(str, ": ", 2); len(v) == 2 {
			str = v[1]
		}
		if strings.Contains(str, "Using interface") {
			if v := strings.FieldsFunc(str, util.SplitFunc); len(v) > 0 {
				l.name = v[len(v)-1]
			}
		}
		if strings.Contains(str, "IPCP: myaddr") {
			close(l.pppUp)
		}
		colorlog.Print(color.HiGreenString(str))
	}
}


================================================
FILE: pkg/util/util.go
================================================
package util

func SplitFunc(c rune) bool {
	return c == ' ' || c == '\n' || c == '\r'
}

func StrSliceContains(haystack []string, needle string) bool {
	for _, s := range haystack {
		if s == needle {
			return true
		}
	}
	return false
}
Download .txt
gitextract_zfkw6760/

├── .github/
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── go-test.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── README.md
├── SIGNATURE.md
├── cmd/
│   └── gof5/
│       ├── gof5.manifest
│       ├── gof5_windows.syso
│       ├── main.go
│       ├── root_linux.go
│       ├── root_others.go
│       └── root_windows.go
├── go.mod
├── go.sum
├── org.freedesktop.resolve1.pkla
└── pkg/
    ├── client/
    │   ├── client.go
    │   ├── http.go
    │   ├── http_test.go
    │   └── logger.go
    ├── config/
    │   ├── config.go
    │   ├── types.go
    │   ├── wintun_other.go
    │   └── wintun_windows.go
    ├── cookie/
    │   └── cookie.go
    ├── dns/
    │   └── dns.go
    ├── link/
    │   ├── cmd_nix.go
    │   ├── cmd_windows.go
    │   ├── f5.go
    │   ├── link.go
    │   └── pppd.go
    └── util/
        └── util.go
Download .txt
SYMBOL INDEX (110 symbols across 20 files)

FILE: cmd/gof5/main.go
  function fatal (line 19) | func fatal(err error) {
  function main (line 30) | func main() {

FILE: cmd/gof5/root_linux.go
  function checkCapability (line 14) | func checkCapability(c *cap.Set, capability cap.Value) error {
  function checkPermissions (line 43) | func checkPermissions() error {

FILE: cmd/gof5/root_others.go
  function checkPermissions (line 11) | func checkPermissions() error {

FILE: cmd/gof5/root_windows.go
  function checkPermissions (line 12) | func checkPermissions() error {

FILE: pkg/client/client.go
  type Options (line 22) | type Options struct
  function UrlHandlerF5Vpn (line 40) | func UrlHandlerF5Vpn(opts *Options, s string) error {
  function Connect (line 85) | func Connect(opts *Options) error {

FILE: pkg/client/http.go
  constant userAgent (line 31) | userAgent        = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1a2pr...
  constant androidUserAgent (line 32) | androidUserAgent = "Mozilla/5.0 (Linux; Android 10; SM-G975F Build/QP1A....
  function tlsConfig (line 35) | func tlsConfig(opts *Options, insecure bool) (*tls.Config, error) {
  function readFile (line 71) | func readFile(path string) ([]byte, error) {
  function checkRedirect (line 96) | func checkRedirect(c *http.Client) func(*http.Request, []*http.Request) ...
  function generateClientData (line 118) | func generateClientData(cData config.ClientData) (string, error) {
  function loginSignature (line 154) | func loginSignature(c *http.Client, server string, _, _ *string) error {
  function login (line 217) | func login(c *http.Client, server string, username, password *string) er...
  function parseProfile (line 282) | func parseProfile(reader io.ReadCloser, profileIndex int, profileName st...
  function getProfiles (line 311) | func getProfiles(c *http.Client, server string) (*http.Response, error) {
  function getConnectionOptions (line 320) | func getConnectionOptions(c *http.Client, opts *Options, profile string)...
  function closeVPNSession (line 363) | func closeVPNSession(c *http.Client, server string) {
  function getServersList (line 376) | func getServersList(c *http.Client, server string) (*url.URL, error) {

FILE: pkg/client/http_test.go
  function TestSignature (line 10) | func TestSignature(t *testing.T) {
  function TestUnmarshal (line 22) | func TestUnmarshal(t *testing.T) {

FILE: pkg/client/logger.go
  type Logger (line 14) | type Logger interface
  type logger (line 19) | type logger struct
    method RequestPrintf (line 23) | func (lg logger) RequestPrintf(format string, args ...interface{}) {
    method ResponsePrintf (line 29) | func (lg logger) ResponsePrintf(format string, args ...interface{}) {
  type noopLogger (line 36) | type noopLogger struct
    method RequestPrintf (line 39) | func (noopLogger) RequestPrintf(format string, args ...interface{}) {}
    method ResponsePrintf (line 42) | func (noopLogger) ResponsePrintf(format string, args ...interface{}) {}
  type RoundTripper (line 46) | type RoundTripper struct
    method formatHeaders (line 55) | func (rt *RoundTripper) formatHeaders(headers http.Header, separator s...
    method RoundTrip (line 68) | func (rt *RoundTripper) RoundTrip(request *http.Request) (*http.Respon...
    method logRequest (line 116) | func (rt *RoundTripper) logRequest(original io.ReadCloser, contentType...
    method logResponse (line 131) | func (rt *RoundTripper) logResponse(original io.ReadCloser, contentTyp...
    method log (line 144) | func (rt *RoundTripper) log() Logger {

FILE: pkg/config/config.go
  constant configDir (line 20) | configDir  = ".gof5"
  constant configName (line 21) | configName = "config.yaml"
  function ReadConfig (line 31) | func ReadConfig(debug bool) (*Config, error) {

FILE: pkg/config/types.go
  type Config (line 17) | type Config struct
    method UnmarshalYAML (line 48) | func (r *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
  type Favorite (line 114) | type Favorite struct
  type Bool (line 118) | type Bool
    method String (line 120) | func (b Bool) String() string {
    method MarshalXML (line 127) | func (b Bool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
  function strToBool (line 131) | func strToBool(s string) (Bool, error) {
  type Object (line 143) | type Object struct
    method UnmarshalXML (line 198) | func (o *Object) UnmarshalXML(d *xml.Decoder, start xml.StartElement) ...
  type TrafficControl (line 173) | type TrafficControl struct
  type Flow (line 177) | type Flow struct
  type Filter (line 188) | type Filter struct
  type Session (line 245) | type Session struct
  type Profiles (line 253) | type Profiles struct
  type FavoriteItem (line 259) | type FavoriteItem struct
  type Hostname (line 266) | type Hostname
    method MarshalXML (line 268) | func (h Hostname) MarshalXML(e *xml.Encoder, start xml.StartElement) e...
  function processIPs (line 272) | func processIPs(ips string, length int) []net.IP {
  function parseCIDRs (line 290) | func parseCIDRs(cidrs []string, length int) ([]*net.IPNet, error) {
  function processCIDRs (line 323) | func processCIDRs(cidrs string, length int) []*net.IPNet {
  function subnetsToIPSet (line 355) | func subnetsToIPSet(subnets []*net.IPNet) *netaddr.IPSet {
  function inverseCIDRs4 (line 367) | func inverseCIDRs4(exclude []*net.IPNet) *netaddr.IPSet {
  type AgentInfo (line 410) | type AgentInfo struct
  type ClientData (line 437) | type ClientData struct
  type PreConfigProfile (line 445) | type PreConfigProfile struct
  type Server (line 453) | type Server struct
  type preConfigSession (line 458) | type preConfigSession struct
    method UnmarshalXML (line 468) | func (o *preConfigSession) UnmarshalXML(d *xml.Decoder, start xml.Star...
  type PasswordPolicy (line 513) | type PasswordPolicy struct
  type Update (line 518) | type Update struct
    method UnmarshalXML (line 522) | func (o *Update) UnmarshalXML(d *xml.Decoder, start xml.StartElement) ...

FILE: pkg/config/wintun_other.go
  function checkWinTunDriver (line 6) | func checkWinTunDriver() error {

FILE: pkg/config/wintun_windows.go
  constant winTun (line 15) | winTun     = "wintun.dll"
  constant winTunSite (line 16) | winTunSite = "https://www.wintun.net/"
  function checkWinTunDriver (line 19) | func checkWinTunDriver() error {

FILE: pkg/cookie/cookie.go
  constant cookiesName (line 20) | cookiesName = "cookies.yaml"
  function parseCookies (line 22) | func parseCookies(configPath string) map[string][]string {
  function ReadCookies (line 42) | func ReadCookies(c *http.Client, u *url.URL, cfg *config.Config, session...
  function SaveCookies (line 64) | func SaveCookies(c *http.Client, u *url.URL, cfg *config.Config) error {

FILE: pkg/dns/dns.go
  function Start (line 14) | func Start(cfg *config.Config, errChan chan error, tunDown chan struct{}) {
  function dnsHandler (line 56) | func dnsHandler(w dns.ResponseWriter, m *dns.Msg, cfg *config.Config, pr...
  function handleCustom (line 77) | func handleCustom(w dns.ResponseWriter, o *dns.Msg, c *dns.Client, ip ne...

FILE: pkg/link/cmd_nix.go
  function Cmd (line 15) | func Cmd(cfg *config.Config) *exec.Cmd {

FILE: pkg/link/cmd_windows.go
  function Cmd (line 12) | func Cmd(_ *config.Config) *exec.Cmd {

FILE: pkg/link/f5.go
  function readBuf (line 16) | func readBuf(buf, sep []byte) []byte {
  function bytesToIPv4 (line 64) | func bytesToIPv4(bytes []byte) net.IP {
  function bytesToIPv6 (line 68) | func bytesToIPv6(bytes []byte) net.IP {
  function processPPP (line 72) | func processPPP(l *vpnLink, buf []byte, dstBuf *bytes.Buffer) error {
  function fromF5 (line 431) | func fromF5(l *vpnLink, dstBuf *bytes.Buffer) error {
  method HttpToTun (line 465) | func (l *vpnLink) HttpToTun() {
  function toF5 (line 481) | func toF5(l *vpnLink, buf []byte, dst *bytes.Buffer) error {
  method TunToHTTP (line 539) | func (l *vpnLink) TunToHTTP() {

FILE: pkg/link/link.go
  constant bufferSize (line 29) | bufferSize   = 1500
  constant userAgentVPN (line 30) | userAgentVPN = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trid...
  type vpnLink (line 35) | type vpnLink struct
    method createTunDevice (line 162) | func (l *vpnLink) createTunDevice() error {
    method configureDNS (line 205) | func (l *vpnLink) configureDNS(cfg *config.Config) error {
    method WaitAndConfig (line 287) | func (l *vpnLink) WaitAndConfig(cfg *config.Config) {
    method RestoreConfig (line 367) | func (l *vpnLink) RestoreConfig(cfg *config.Config) {
  function randomHostname (line 59) | func randomHostname(n int) []byte {
  function InitConnection (line 72) | func InitConnection(server string, cfg *config.Config, tlsConfig *tls.Co...

FILE: pkg/link/pppd.go
  method decodeHDLC (line 25) | func (l *vpnLink) decodeHDLC(buf []byte, src string) {
  method PppdHTTPToTun (line 50) | func (l *vpnLink) PppdHTTPToTun(pppd io.WriteCloser) {
  method PppdTunToHTTP (line 81) | func (l *vpnLink) PppdTunToHTTP(pppd io.ReadCloser) {
  method CatchPPPDTermination (line 112) | func (l *vpnLink) CatchPPPDTermination(cmd *exec.Cmd) {
  method StopPPPDChild (line 121) | func (l *vpnLink) StopPPPDChild(cmd *exec.Cmd) {
  method PppdLogParser (line 129) | func (l *vpnLink) PppdLogParser(stderr io.Reader) {
  method PppLogParser (line 148) | func (l *vpnLink) PppLogParser() {

FILE: pkg/util/util.go
  function SplitFunc (line 3) | func SplitFunc(c rune) bool {
  function StrSliceContains (line 7) | func StrSliceContains(haystack []string, needle string) bool {
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (137K chars).
[
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2323,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/go-test.yml",
    "chars": 380,
    "preview": "name: Go Unit Tests\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  golang-test:\n    runs-on: ubuntu-"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2319,
    "preview": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n      commit:\n  push:\n    tags:\n      - v*\n\npermissions:\n"
  },
  {
    "path": ".gitignore",
    "chars": 31,
    "preview": "gopath\nbin\ncookies\nroutes.yaml\n"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 1052,
    "preview": "version: 2\nbuilds:\n  - id: ubuntu-latest\n    main: ./cmd/gof5\n    goos: [linux]\n    goarch: [amd64]\n    flags:\n      - -"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Makefile",
    "chars": 831,
    "preview": "PKG:=github.com/kayrus/gof5\nAPP_NAME:=gof5\nPWD:=$(shell pwd)\nUID:=$(shell id -u)\nVERSION:=$(shell git describe --tags --"
  },
  {
    "path": "README.md",
    "chars": 5230,
    "preview": "# gof5\n\n## Requirements\n\n* an application must be executed under a privileged user\n\n## Linux\n\nIf your Linux distribution"
  },
  {
    "path": "SIGNATURE.md",
    "chars": 1687,
    "preview": "# Signature\n\n* F5 client requests a token from a server: `/my.logon.php3?outform=xml&client_version=2.0&get_token=1`\n* F"
  },
  {
    "path": "cmd/gof5/gof5.manifest",
    "chars": 538,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersi"
  },
  {
    "path": "cmd/gof5/main.go",
    "chars": 1930,
    "preview": "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\tV"
  },
  {
    "path": "cmd/gof5/root_linux.go",
    "chars": 1959,
    "preview": "//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/l"
  },
  {
    "path": "cmd/gof5/root_others.go",
    "chars": 229,
    "preview": "//go:build !windows && !linux\n// +build !windows,!linux\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc checkPermissions() "
  },
  {
    "path": "cmd/gof5/root_windows.go",
    "chars": 970,
    "preview": "//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 checkPermission"
  },
  {
    "path": "go.mod",
    "chars": 1830,
    "preview": "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\tgith"
  },
  {
    "path": "go.sum",
    "chars": 15608,
    "preview": "github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0=\ngithub.com/IBM/netaddr v1.5.0/go.mod h1:DD"
  },
  {
    "path": "org.freedesktop.resolve1.pkla",
    "chars": 167,
    "preview": "[Adding or changing system-wide resolved]\nIdentity=unix-group:netdev;unix-group:sudo\nAction=org.freedesktop.resolve1.*\nR"
  },
  {
    "path": "pkg/client/client.go",
    "chars": 7110,
    "preview": "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\"o"
  },
  {
    "path": "pkg/client/http.go",
    "chars": 10982,
    "preview": "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\"encodin"
  },
  {
    "path": "pkg/client/http_test.go",
    "chars": 1765,
    "preview": "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 *testi"
  },
  {
    "path": "pkg/client/logger.go",
    "chars": 4064,
    "preview": "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 r"
  },
  {
    "path": "pkg/config/config.go",
    "chars": 3353,
    "preview": "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\"gi"
  },
  {
    "path": "pkg/config/types.go",
    "chars": 14368,
    "preview": "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/kay"
  },
  {
    "path": "pkg/config/wintun_other.go",
    "chars": 103,
    "preview": "//go:build !windows\n// +build !windows\n\npackage config\n\nfunc checkWinTunDriver() error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/config/wintun_windows.go",
    "chars": 537,
    "preview": "//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"
  },
  {
    "path": "pkg/cookie/cookie.go",
    "chars": 2124,
    "preview": "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"
  },
  {
    "path": "pkg/dns/dns.go",
    "chars": 1891,
    "preview": "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\n"
  },
  {
    "path": "pkg/link/cmd_nix.go",
    "chars": 1040,
    "preview": "//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/kay"
  },
  {
    "path": "pkg/link/cmd_windows.go",
    "chars": 166,
    "preview": "//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 Cm"
  },
  {
    "path": "pkg/link/f5.go",
    "chars": 13875,
    "preview": "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"
  },
  {
    "path": "pkg/link/link.go",
    "chars": 10008,
    "preview": "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\"r"
  },
  {
    "path": "pkg/link/pppd.go",
    "chars": 4034,
    "preview": "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."
  },
  {
    "path": "pkg/util/util.go",
    "chars": 240,
    "preview": "package util\n\nfunc SplitFunc(c rune) bool {\n\treturn c == ' ' || c == '\\n' || c == '\\r'\n}\n\nfunc StrSliceContains(haystack"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the kayrus/gof5 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (121.2 KB), approximately 41.3k tokens, and a symbol index with 110 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!