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: `12.0/my.policy16384` * F5 client generates an **XML** with client parameters: ```xml standalone 2.0 Linux x64 no no no / no dGVzdA== // base64("test") ``` Actual string: `standalone2.0Linuxx64nonono/nodGVzdA==` * 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 ================================================ gof5 requires Administrator privileges ================================================ 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(">") data = []byte(r.ReplaceAllString(string(data), ">")) } 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(`
https://f5-1.com
One
https://f5-2.com
Two
YESNONONODISK240YEScorp.intcorp
`) 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 }