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
}
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
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.