├── .gitignore
├── cmd
└── gof5
│ ├── gof5_windows.syso
│ ├── root_others.go
│ ├── gof5.manifest
│ ├── root_windows.go
│ ├── main.go
│ └── root_linux.go
├── pkg
├── config
│ ├── wintun_other.go
│ ├── wintun_windows.go
│ ├── config.go
│ └── types.go
├── link
│ ├── cmd_windows.go
│ ├── cmd_nix.go
│ ├── pppd.go
│ ├── link.go
│ └── f5.go
├── util
│ └── util.go
├── client
│ ├── http_test.go
│ ├── logger.go
│ ├── client.go
│ └── http.go
├── dns
│ └── dns.go
└── cookie
│ └── cookie.go
├── org.freedesktop.resolve1.pkla
├── Makefile
├── .goreleaser.yml
├── SIGNATURE.md
├── go.mod
├── .github
└── workflows
│ ├── release.yml
│ └── codeql-analysis.yml
├── README.md
├── LICENSE
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | gopath
2 | bin
3 | cookies
4 | routes.yaml
5 |
--------------------------------------------------------------------------------
/cmd/gof5/gof5_windows.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kayrus/gof5/HEAD/cmd/gof5/gof5_windows.syso
--------------------------------------------------------------------------------
/pkg/config/wintun_other.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package config
5 |
6 | func checkWinTunDriver() error {
7 | return nil
8 | }
9 |
--------------------------------------------------------------------------------
/org.freedesktop.resolve1.pkla:
--------------------------------------------------------------------------------
1 | [Adding or changing system-wide resolved]
2 | Identity=unix-group:netdev;unix-group:sudo
3 | Action=org.freedesktop.resolve1.*
4 | ResultAny=no
5 | ResultInactive=no
6 | ResultActive=yes
7 |
--------------------------------------------------------------------------------
/pkg/link/cmd_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package link
5 |
6 | import (
7 | "os/exec"
8 |
9 | "github.com/kayrus/gof5/pkg/config"
10 | )
11 |
12 | func Cmd(_ *config.Config) *exec.Cmd {
13 | return nil
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | func SplitFunc(c rune) bool {
4 | return c == ' ' || c == '\n' || c == '\r'
5 | }
6 |
7 | func StrSliceContains(haystack []string, needle string) bool {
8 | for _, s := range haystack {
9 | if s == needle {
10 | return true
11 | }
12 | }
13 | return false
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/gof5/root_others.go:
--------------------------------------------------------------------------------
1 | //go:build !windows && !linux
2 | // +build !windows,!linux
3 |
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | )
10 |
11 | func checkPermissions() error {
12 | if uid := os.Getuid(); uid != 0 {
13 | return fmt.Errorf("gof5 needs to run as root")
14 | }
15 | return nil
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/gof5/gof5.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | gof5 requires Administrator privileges
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/pkg/config/wintun_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package config
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | "path/filepath"
10 |
11 | "golang.org/x/sys/windows"
12 | )
13 |
14 | const (
15 | winTun = "wintun.dll"
16 | winTunSite = "https://www.wintun.net/"
17 | )
18 |
19 | func checkWinTunDriver() error {
20 | err := windows.NewLazyDLL(winTun).Load()
21 | if err != nil {
22 | dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
23 | if err != nil {
24 | dir = "gof5"
25 | }
26 | return fmt.Errorf("the %s was not found, you can download it from %s and place it into the %q directory", winTun, winTunSite, dir)
27 | }
28 |
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PKG:=github.com/kayrus/gof5
2 | APP_NAME:=gof5
3 | PWD:=$(shell pwd)
4 | UID:=$(shell id -u)
5 | VERSION:=$(shell git describe --tags --always --dirty="-dev")
6 | GOOS:=$(shell go env GOOS)
7 | LDFLAGS:=-X main.Version=$(VERSION) -w -s
8 | GOOS:=$(strip $(shell go env GOOS))
9 | GOARCHs:=$(strip $(shell go env GOARCH))
10 |
11 | ifeq "$(GOOS)" "windows"
12 | SUFFIX=.exe
13 | endif
14 |
15 | # CGO must be enabled
16 | export CGO_ENABLED:=1
17 |
18 | build: fmt vet
19 | $(foreach GOARCH,$(GOARCHs),$(shell GOARCH=$(GOARCH) go build -ldflags="$(LDFLAGS)" -trimpath -o bin/$(APP_NAME)_$(GOOS)_$(GOARCH)$(SUFFIX) ./cmd/gof5))
20 |
21 | docker:
22 | docker pull golang:latest
23 | docker run -ti --rm -e GOCACHE=/tmp -v $(PWD):/$(APP_NAME) -u $(UID):$(UID) --workdir /$(APP_NAME) golang:latest make
24 |
25 | fmt:
26 | gofmt -s -w cmd pkg
27 |
28 | vet:
29 | go vet ./...
30 |
31 | static:
32 | staticcheck ./cmd/... ./pkg/...
33 |
34 | test:
35 | go test -v ./cmd/... ./pkg/...
36 |
--------------------------------------------------------------------------------
/cmd/gof5/root_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package main
5 |
6 | import (
7 | "fmt"
8 |
9 | "golang.org/x/sys/windows"
10 | )
11 |
12 | func checkPermissions() error {
13 | // https://github.com/golang/go/issues/28804#issuecomment-505326268
14 | var sid *windows.SID
15 |
16 | // https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
17 | err := windows.AllocateAndInitializeSid(
18 | &windows.SECURITY_NT_AUTHORITY,
19 | 2,
20 | windows.SECURITY_BUILTIN_DOMAIN_RID,
21 | windows.DOMAIN_ALIAS_RID_ADMINS,
22 | 0, 0, 0, 0, 0, 0,
23 | &sid)
24 | if err != nil {
25 | return fmt.Errorf("error while checking for elevated permissions: %s", err)
26 | }
27 |
28 | // We must free the sid to prevent security token leaks
29 | defer windows.FreeSid(sid)
30 | token := windows.Token(0)
31 |
32 | member, err := token.IsMember(sid)
33 | if err != nil {
34 | return fmt.Errorf("error while checking for elevated permissions: %s", err)
35 | }
36 | if !member {
37 | return fmt.Errorf("gof5 needs to run with administrator permissions")
38 | }
39 |
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/link/cmd_nix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package link
5 |
6 | import (
7 | "log"
8 | "os/exec"
9 | "runtime"
10 | "syscall"
11 |
12 | "github.com/kayrus/gof5/pkg/config"
13 | )
14 |
15 | func Cmd(cfg *config.Config) *exec.Cmd {
16 | var cmd *exec.Cmd
17 | if cfg.Driver == "pppd" {
18 | // VPN
19 | if cfg.IPv6 && bool(cfg.F5Config.Object.IPv6) {
20 | cfg.PPPdArgs = append(cfg.PPPdArgs,
21 | "ipv6cp-accept-local",
22 | "ipv6cp-accept-remote",
23 | "+ipv6",
24 | )
25 | } else {
26 | cfg.PPPdArgs = append(cfg.PPPdArgs,
27 | // TODO: clarify why it doesn't work
28 | "noipv6", // Unsupported protocol 'IPv6 Control Protocol' (0x8057) received
29 | )
30 | }
31 | if cfg.Debug {
32 | cfg.PPPdArgs = append(cfg.PPPdArgs,
33 | "debug",
34 | "kdebug", "1",
35 | )
36 | log.Printf("pppd args: %q", cfg.PPPdArgs)
37 | }
38 |
39 | switch runtime.GOOS {
40 | default:
41 | cmd = exec.Command("pppd", cfg.PPPdArgs...)
42 | case "freebsd":
43 | cmd = exec.Command("ppp", "-direct")
44 | }
45 |
46 | // don't forward parent process signals to a child process
47 | cmd.SysProcAttr = &syscall.SysProcAttr{
48 | Setpgid: true,
49 | Pgid: 0,
50 | }
51 | return cmd
52 | }
53 | return nil
54 | }
55 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | builds:
3 | - id: ubuntu-latest
4 | main: ./cmd/gof5
5 | goos: [linux]
6 | goarch: [amd64]
7 | flags:
8 | - -trimpath
9 | ldflags:
10 | - -s -w -X main.Version=v{{ .Version }}
11 | env:
12 | - CGO_ENABLED=1
13 |
14 | - id: windows-latest
15 | main: ./cmd/gof5
16 | goos: [windows]
17 | goarch: [amd64]
18 | flags:
19 | - -trimpath
20 | ldflags:
21 | - -s -w -X main.Version=v{{ .Version }}
22 | env:
23 | - CGO_ENABLED=1
24 |
25 | - id: macos-13
26 | main: ./cmd/gof5
27 | goos: [darwin]
28 | goarch: [amd64]
29 | flags:
30 | - -trimpath
31 | ldflags:
32 | - -s -w -X main.Version=v{{ .Version }}
33 | env:
34 | - CGO_ENABLED=1
35 |
36 | - id: macos-latest
37 | main: ./cmd/gof5
38 | goos: [darwin]
39 | goarch: [arm64]
40 | flags:
41 | - -trimpath
42 | ldflags:
43 | - -s -w -X main.Version=v{{ .Version }}
44 | env:
45 | - CGO_ENABLED=1
46 |
47 | archives:
48 | - formats: [binary]
49 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
50 |
51 | checksum:
52 | split: true
53 |
54 | release:
55 | draft: true
56 | use_existing_draft: true
57 | replace_existing_draft: false
58 |
59 | changelog:
60 | disable: true
61 |
--------------------------------------------------------------------------------
/SIGNATURE.md:
--------------------------------------------------------------------------------
1 | # Signature
2 |
3 | * F5 client requests a token from a server: `/my.logon.php3?outform=xml&client_version=2.0&get_token=1`
4 | * F5 server sends a **token** to a client: `12.0/my.policy16384`
5 | * F5 client generates an **XML** with client parameters:
6 |
7 | ```xml
8 |
9 | standalone
10 | 2.0
11 | Linux
12 | x64
13 | no
14 | no
15 | no
16 | /
17 | no
18 | dGVzdA== // base64("test")
19 |
20 |
21 | ```
22 |
23 | Actual string:
24 |
25 | `standalone2.0Linuxx64nonono/nodGVzdA==`
26 |
27 | * 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")))
28 | * both **XML** and **signature** are base64 encoded and put into parameters:
29 |
30 | `client_data = sprintf(str, "session=%s&device_info=%s&agent_result=%s&token=%s&signature=%s", "", base64(xml), "", token, signature)`
31 |
32 | * The **client\_data** string generated above is also base64 encoded and then sent as a POST request to F5 `/my.policy`:
33 |
34 | `post_request = sprintf(str, "client_data=%s", base64(client_data))`
35 |
--------------------------------------------------------------------------------
/pkg/client/http_test.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "encoding/xml"
5 | "testing"
6 |
7 | "github.com/kayrus/gof5/pkg/config"
8 | )
9 |
10 | func TestSignature(t *testing.T) {
11 | s, err := generateClientData(config.ClientData{Token: "1"})
12 | if err != nil {
13 | t.Errorf("Signature is wrong: %s", err)
14 | }
15 |
16 | expected := "c2Vzc2lvbj0mZGV2aWNlX2luZm89UEdGblpXNTBYMmx1Wm04K1BIUjVjR1UrYzNSaGJtUmhiRzl1WlR3dmRIbHdaVDQ4ZG1WeWMybHZiajR5TGpBOEwzWmxjbk5wYjI0K1BIQnNZWFJtYjNKdFBreHBiblY0UEM5d2JHRjBabTl5YlQ0OFkzQjFQbmcyTkR3dlkzQjFQanhxWVhaaGMyTnlhWEIwUG01dlBDOXFZWFpoYzJOeWFYQjBQanhoWTNScGRtVjRQbTV2UEM5aFkzUnBkbVY0UGp4d2JIVm5hVzQrYm04OEwzQnNkV2RwYmo0OGJHRnVaR2x1WjNWeWFUNHZQQzlzWVc1a2FXNW5kWEpwUGp4c2IyTnJaV1J0YjJSbFBtNXZQQzlzYjJOclpXUnRiMlJsUGp4b2IzTjBibUZ0WlQ1a1IxWjZaRUU5UFR3dmFHOXpkRzVoYldVK1BHRndjRjlwWkQ0OEwyRndjRjlwWkQ0OEwyRm5aVzUwWDJsdVptOCsmYWdlbnRfcmVzdWx0PSZ0b2tlbj0xJnNpZ25hdHVyZT00c1krcFFkM3pyUTVjMkZsNUJ3a0JnPT0="
17 | if s != expected {
18 | t.Errorf("Client data doesn't correspond to expected: %s", s)
19 | }
20 | }
21 |
22 | func TestUnmarshal(t *testing.T) {
23 | // parse https://f5.com/pre/config.php
24 | b := []byte(`https://f5-1.comOnehttps://f5-2.comTwoYESNONONODISK240YEScorp.intcorp`)
25 | var s config.PreConfigProfile
26 | if err := xml.Unmarshal(b, &s); err != nil {
27 | t.Errorf("failed to unmarshal a response: %s", err)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kayrus/gof5
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/IBM/netaddr v1.5.0
7 | github.com/fatih/color v1.10.0
8 | github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
9 | github.com/hpcloud/tail v1.0.0
10 | github.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382
11 | github.com/manifoldco/promptui v0.8.0
12 | github.com/miekg/dns v1.1.40
13 | github.com/mitchellh/go-homedir v1.1.0
14 | github.com/pion/dtls/v2 v2.2.4
15 | github.com/zaninime/go-hdlc v1.1.1
16 | golang.org/x/net v0.47.0
17 | golang.org/x/sys v0.38.0
18 | gopkg.in/yaml.v2 v2.4.0
19 | kernel.org/pub/linux/libs/security/libcap/cap v1.2.48
20 | )
21 |
22 | require (
23 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
24 | github.com/fsnotify/fsnotify v1.6.0 // indirect
25 | github.com/godbus/dbus/v5 v5.0.6 // indirect
26 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
27 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect
28 | github.com/mattn/go-colorable v0.1.8 // indirect
29 | github.com/mattn/go-isatty v0.0.12 // indirect
30 | github.com/pion/logging v0.2.2 // indirect
31 | github.com/pion/transport/v2 v2.0.0 // indirect
32 | github.com/pion/udp v0.1.4 // indirect
33 | github.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162 // indirect
34 | github.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f // indirect
35 | github.com/vishvananda/netlink v1.1.0 // indirect
36 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
37 | golang.org/x/crypto v0.45.0 // indirect
38 | golang.org/x/term v0.37.0 // indirect
39 | golang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71 // indirect
40 | golang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a // indirect
41 | gopkg.in/fsnotify.v1 v1.4.7 // indirect
42 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
43 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.48 // indirect
44 | )
45 |
--------------------------------------------------------------------------------
/cmd/gof5/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "os"
9 | "runtime"
10 |
11 | "github.com/kayrus/gof5/pkg/client"
12 | )
13 |
14 | var (
15 | Version = "dev"
16 | info = fmt.Sprintf("gof5 %s compiled with %s for %s/%s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
17 | )
18 |
19 | func fatal(err error) {
20 | if runtime.GOOS == "windows" {
21 | // Escalated privileges in windows opens a new terminal, and if there is an
22 | // error, it is impossible to see it. Thus we wait for user to press a button.
23 | log.Printf("%s, press enter to exit", err)
24 | bufio.NewReader(os.Stdin).ReadBytes('\n')
25 | os.Exit(1)
26 | }
27 | log.Fatal(err)
28 | }
29 |
30 | func main() {
31 | var version bool
32 | var opts client.Options
33 |
34 | flag.StringVar(&opts.Server, "server", "", "")
35 | flag.StringVar(&opts.Username, "username", "", "")
36 | flag.StringVar(&opts.Password, "password", "", "")
37 | flag.StringVar(&opts.SessionID, "session", "", "Reuse a session ID")
38 | flag.StringVar(&opts.CACert, "ca-cert", "", "Path to a custom CA certificate")
39 | flag.StringVar(&opts.Cert, "cert", "", "Path to a user TLS certificate")
40 | flag.StringVar(&opts.Key, "key", "", "Path to a user TLS key")
41 | flag.BoolVar(&opts.CloseSession, "close-session", false, "Close HTTPS VPN session on exit")
42 | flag.BoolVar(&opts.Debug, "debug", false, "Show debug logs")
43 | flag.BoolVar(&opts.Sel, "select", false, "Select a server from available F5 servers")
44 | flag.IntVar(&opts.ProfileIndex, "profile-index", 0, "If multiple VPN profiles are found chose profile n")
45 | flag.BoolVar(&version, "version", false, "Show version and exit cleanly")
46 |
47 | flag.Parse()
48 |
49 | if version {
50 | fmt.Println(info)
51 | os.Exit(0)
52 | }
53 |
54 | if opts.ProfileIndex < 0 {
55 | fatal(fmt.Errorf("profile-index cannot be negative"))
56 | }
57 |
58 | log.Print(info)
59 |
60 | if err := checkPermissions(); err != nil {
61 | fatal(err)
62 | }
63 |
64 | if flag.NArg() > 0 {
65 | if err := client.UrlHandlerF5Vpn(&opts, flag.Arg(0)); err != nil {
66 | fatal(err)
67 | }
68 | }
69 |
70 | if err := client.Connect(&opts); err != nil {
71 | fatal(err)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/cmd/gof5/root_linux.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 | // +build linux
3 |
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | "strings"
10 |
11 | "kernel.org/pub/linux/libs/security/libcap/cap"
12 | )
13 |
14 | func checkCapability(c *cap.Set, capability cap.Value) error {
15 | // when "setcap capability+ep gof5" was used
16 | capable, err := c.GetFlag(cap.Effective, capability)
17 | if err != nil {
18 | return fmt.Errorf("failed to get process effective capability flag: %v", err)
19 | }
20 | if capable {
21 | return nil
22 | }
23 |
24 | // when "setcap capability+p gof5" or "setcap capability+i gof5" was used and a user has inheritable capability
25 | capable, err = c.GetFlag(cap.Permitted, capability)
26 | if err != nil {
27 | return fmt.Errorf("failed to get process permitted capability flag: %v", err)
28 | }
29 | if capable {
30 | if err = c.SetFlag(cap.Effective, true, capability); err != nil {
31 | return fmt.Errorf("permitted capability detected: failed to set effective %s capability flag: %v", strings.ToUpper(capability.String()), err)
32 | }
33 | if err = c.SetProc(); err != nil {
34 | return fmt.Errorf("permitted capability detected: failed to set effective %s capability: %v", strings.ToUpper(capability.String()), err)
35 | }
36 | return nil
37 | }
38 |
39 | return fmt.Errorf("cannot obtain effective %s capability", strings.ToUpper(capability.String()))
40 | }
41 |
42 | // TODO: detect cap_net_bind_service for DNS bind
43 | func checkPermissions() error {
44 | // check root first
45 | if uid := os.Getuid(); uid == 0 {
46 | return nil
47 | }
48 |
49 | c := cap.GetProc()
50 |
51 | var err error
52 | capabilities := []cap.Value{
53 | cap.NET_ADMIN, // to create and manage tun interface
54 | // no need to run own DNS proxy, when systemd-resolved is used
55 | // cap.NET_BIND_SERVICE, // to bind DNS proxy
56 | }
57 | for _, capability := range capabilities {
58 | err = checkCapability(c, capability)
59 | if err != nil {
60 | break
61 | }
62 | }
63 |
64 | if err == nil {
65 | return nil
66 | }
67 |
68 | // no capabilities or "setcap capability+i gof5" was used and a user has no inheritable capability
69 | return fmt.Errorf("gof5 needs to run with CAP_NET_ADMIN capability or as root: %v", err)
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/dns/dns.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "strings"
8 |
9 | "github.com/kayrus/gof5/pkg/config"
10 |
11 | "github.com/miekg/dns"
12 | )
13 |
14 | func Start(cfg *config.Config, errChan chan error, tunDown chan struct{}) {
15 | dnsUDPHandler := func(w dns.ResponseWriter, m *dns.Msg) {
16 | dnsHandler(w, m, cfg, "udp")
17 | }
18 |
19 | dnsTCPHandler := func(w dns.ResponseWriter, m *dns.Msg) {
20 | dnsHandler(w, m, cfg, "tcp")
21 | }
22 |
23 | listen := net.JoinHostPort(cfg.ListenDNS.String(), "53")
24 | srvUDP := &dns.Server{
25 | Addr: listen,
26 | Net: "udp",
27 | Handler: dns.HandlerFunc(dnsUDPHandler),
28 | }
29 | srvTCP := &dns.Server{
30 | Addr: listen,
31 | Net: "tcp",
32 | Handler: dns.HandlerFunc(dnsTCPHandler),
33 | }
34 |
35 | go func() {
36 | if err := srvUDP.ListenAndServe(); err != nil {
37 | errChan <- fmt.Errorf("failed to set udp listener: %v", err)
38 | return
39 | }
40 | }()
41 | go func() {
42 | if err := srvTCP.ListenAndServe(); err != nil {
43 | errChan <- fmt.Errorf("failed to set tcp listener: %v", err)
44 | return
45 | }
46 | }()
47 |
48 | go func() {
49 | <-tunDown
50 | log.Printf("Shutting down DNS proxy")
51 | srvUDP.Shutdown()
52 | srvTCP.Shutdown()
53 | }()
54 | }
55 |
56 | func dnsHandler(w dns.ResponseWriter, m *dns.Msg, cfg *config.Config, proto string) {
57 | c := new(dns.Client)
58 | for _, suffix := range cfg.DNS {
59 | if strings.HasSuffix(m.Question[0].Name, suffix) {
60 | if cfg.Debug {
61 | log.Printf("Resolving %q using VPN DNS", m.Question[0].Name)
62 | }
63 | for _, s := range cfg.F5Config.Object.DNS {
64 | if err := handleCustom(w, m, c, s); err == nil {
65 | return
66 | }
67 | }
68 | }
69 | }
70 | for _, s := range cfg.DNSServers {
71 | if err := handleCustom(w, m, c, s); err == nil {
72 | return
73 | }
74 | }
75 | }
76 |
77 | func handleCustom(w dns.ResponseWriter, o *dns.Msg, c *dns.Client, ip net.IP) error {
78 | m := new(dns.Msg)
79 | o.CopyTo(m)
80 | r, _, err := c.Exchange(m, net.JoinHostPort(ip.String(), "53"))
81 | if r == nil || err != nil {
82 | return fmt.Errorf("failed to resolve %q", m.Question[0].Name)
83 | }
84 | w.WriteMsg(r)
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/cookie/cookie.go:
--------------------------------------------------------------------------------
1 | package cookie
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "os"
10 | "path/filepath"
11 | "runtime"
12 | "strings"
13 | "syscall"
14 |
15 | "github.com/kayrus/gof5/pkg/config"
16 |
17 | "gopkg.in/yaml.v2"
18 | )
19 |
20 | const cookiesName = "cookies.yaml"
21 |
22 | func parseCookies(configPath string) map[string][]string {
23 | cookies := make(map[string][]string)
24 |
25 | cookiesPath := filepath.Join(configPath, cookiesName)
26 | v, err := ioutil.ReadFile(cookiesPath)
27 | if err != nil {
28 | // skip "no such file or directory" error on the first startup
29 | if e, ok := err.(*os.PathError); !ok || e.Unwrap() != syscall.ENOENT {
30 | log.Printf("Cannot read cookies file: %v", err)
31 | }
32 | return cookies
33 | }
34 |
35 | if err = yaml.Unmarshal(v, &cookies); err != nil {
36 | log.Printf("Cannot parse cookies: %v", err)
37 | }
38 |
39 | return cookies
40 | }
41 |
42 | func ReadCookies(c *http.Client, u *url.URL, cfg *config.Config, sessionID string) {
43 | v := parseCookies(cfg.Path)
44 | if v, ok := v[u.Host]; ok {
45 | var cookies []*http.Cookie
46 | for _, c := range v {
47 | if v := strings.Split(c, "="); len(v) == 2 {
48 | cookies = append(cookies, &http.Cookie{Name: v[0], Value: v[1]})
49 | }
50 | }
51 | c.Jar.SetCookies(u, cookies)
52 | }
53 |
54 | if sessionID != "" {
55 | log.Printf("Overriding session ID from a CLI argument")
56 | // override session ID from CLI parameter
57 | cookies := []*http.Cookie{
58 | {Name: "MRHSession", Value: sessionID},
59 | }
60 | c.Jar.SetCookies(u, cookies)
61 | }
62 | }
63 |
64 | func SaveCookies(c *http.Client, u *url.URL, cfg *config.Config) error {
65 | raw := parseCookies(cfg.Path)
66 | // empty current cookies list
67 | raw[u.Host] = nil
68 | // write down new cookies
69 | for _, c := range c.Jar.Cookies(u) {
70 | raw[u.Host] = append(raw[u.Host], c.String())
71 | }
72 |
73 | cookies, err := yaml.Marshal(&raw)
74 | if err != nil {
75 | return fmt.Errorf("cannot marshal cookies: %v", err)
76 | }
77 |
78 | cookiesPath := filepath.Join(cfg.Path, cookiesName)
79 | if err = ioutil.WriteFile(cookiesPath, cookies, 0600); err != nil {
80 | return fmt.Errorf("failed to save cookies: %s", err)
81 | }
82 |
83 | if runtime.GOOS != "windows" {
84 | if err = os.Chown(cookiesPath, cfg.Uid, cfg.Gid); err != nil {
85 | return fmt.Errorf("failed to set an owner for cookies file: %s", err)
86 | }
87 | }
88 |
89 | return nil
90 | }
91 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | tag:
7 | commit:
8 | push:
9 | tags:
10 | - v*
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | release:
17 | runs-on: ${{ matrix.os }}
18 | strategy:
19 | matrix:
20 | os: [ubuntu-latest, macos-latest, windows-latest, macos-13]
21 | steps:
22 | - uses: actions/checkout@v4
23 | if: github.event.inputs.commit != ''
24 | with:
25 | # checkout the commit if provided
26 | ref: ${{ github.event.inputs.commit }}
27 | # unshallow the repository to ensure all tags are available
28 | fetch-depth: 0
29 |
30 | - uses: actions/checkout@v4
31 | if: github.event.inputs.commit == ''
32 | with:
33 | # checkout the tag if provided, otherwise checkout the current ref
34 | ref: ${{ github.event.inputs.tag != '' && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }}
35 |
36 | # workaround for Pro feature https://goreleaser.com/customization/nightlies/
37 | # create a dirty tag if the commit is not tagged
38 | - name: Get dirty git tag
39 | id: dirty_tag
40 | if: github.event.inputs.commit != ''
41 | shell: bash
42 | run: echo "tag=$(git tag --points-at HEAD | grep -q . || git describe --tags --always --abbrev=8 --dirty)" >> "$GITHUB_OUTPUT"
43 | - name: Set dirty git tag
44 | if: steps.dirty_tag.outputs.tag != ''
45 | run: git tag ${{ steps.dirty_tag.outputs.tag }}
46 |
47 | - uses: actions/setup-go@v5
48 | with:
49 | go-version-file: 'go.mod'
50 |
51 | - name: Setup yq
52 | if: runner.os == 'Windows'
53 | uses: dcarbone/install-yq-action@v1
54 |
55 | # workaround for Pro feature https://goreleaser.com/customization/prebuilt/
56 | # and the inability to run `goreleaser release --id ${matrix.os}`
57 | - name: Copy goreleaser config to temp location
58 | run: cp .goreleaser.yml ${{ runner.temp }}/.goreleaser.yml
59 | # remove all builds except the one for the current OS
60 | - name: Override builds in copied config
61 | run: yq${{ runner.os == 'Windows' && '.exe' || '' }} -i eval '.builds |= map(select(.id == "${{ matrix.os }}"))' ${{ runner.temp }}/.goreleaser.yml
62 |
63 | - uses: goreleaser/goreleaser-action@v6
64 | with:
65 | args: release --clean --config ${{ runner.temp }}/.goreleaser.yml
66 | env:
67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '16 13 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/pkg/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net"
8 | "os"
9 | "os/user"
10 | "path/filepath"
11 | "runtime"
12 | "strconv"
13 |
14 | "github.com/kayrus/gof5/pkg/util"
15 |
16 | "gopkg.in/yaml.v2"
17 | )
18 |
19 | const (
20 | configDir = ".gof5"
21 | configName = "config.yaml"
22 | )
23 |
24 | var (
25 | defaultDNSListenAddr = net.IPv4(127, 0, 0, 0xf5).To4()
26 | // BSD systems don't support listeniing on 127.0.0.1+N
27 | defaultBSDDNSListenAddr = net.IPv4(127, 0, 0, 1).To4()
28 | supportedDrivers = []string{"wireguard", "pppd"}
29 | )
30 |
31 | func ReadConfig(debug bool) (*Config, error) {
32 | var err error
33 | var usr *user.User
34 |
35 | // resolve sudo user ID
36 | if id, sudoUID := os.Geteuid(), os.Getenv("SUDO_UID"); id == 0 && sudoUID != "" {
37 | usr, err = user.LookupId(sudoUID)
38 | if err != nil {
39 | log.Printf("failed to lookup user ID: %s", err)
40 | if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
41 | usr, err = user.Lookup(sudoUser)
42 | if err != nil {
43 | return nil, fmt.Errorf("failed to lookup user name: %s", err)
44 | }
45 | }
46 | }
47 | } else {
48 | // detect home directory
49 | usr, err = user.Current()
50 | if err != nil {
51 | return nil, fmt.Errorf("failed to detect home directory: %s", err)
52 | }
53 | }
54 | configPath := filepath.Join(usr.HomeDir, configDir)
55 |
56 | var uid, gid int
57 | // windows preserves the original user parameters, no need to detect uid/gid
58 | if runtime.GOOS != "windows" {
59 | uid, err = strconv.Atoi(usr.Uid)
60 | if err != nil {
61 | return nil, fmt.Errorf("failed to convert %q UID to integer: %s", usr.Uid, err)
62 | }
63 | gid, err = strconv.Atoi(usr.Gid)
64 | if err != nil {
65 | return nil, fmt.Errorf("failed to convert %q GID to integer: %s", usr.Uid, err)
66 | }
67 | }
68 |
69 | if _, err := os.Stat(configPath); os.IsNotExist(err) {
70 | log.Printf("%q directory doesn't exist, creating...", configPath)
71 | if err := os.Mkdir(configPath, 0700); err != nil {
72 | return nil, fmt.Errorf("failed to create %q config directory: %s", configPath, err)
73 | }
74 | // windows preserves the original user parameters, no need to chown
75 | if runtime.GOOS != "windows" {
76 | if err := os.Chown(configPath, uid, gid); err != nil {
77 | return nil, fmt.Errorf("failed to set an owner for the %q config directory: %s", configPath, err)
78 | }
79 | }
80 | } else if err != nil {
81 | return nil, fmt.Errorf("failed to get %q directory stat: %s", configPath, err)
82 | }
83 |
84 | cfg := &Config{}
85 | // read config file
86 | // if config doesn't exist, use defaults
87 | if raw, err := ioutil.ReadFile(filepath.Join(configPath, configName)); err == nil {
88 | if err = yaml.Unmarshal(raw, cfg); err != nil {
89 | return nil, fmt.Errorf("cannot parse %s file: %v", configName, err)
90 | }
91 | } else {
92 | log.Printf("Cannot read config file: %s", err)
93 | }
94 |
95 | // set default driver
96 | if cfg.Driver == "" {
97 | cfg.Driver = "wireguard"
98 | }
99 |
100 | if cfg.Driver == "wireguard" {
101 | if err := checkWinTunDriver(); err != nil {
102 | return nil, err
103 | }
104 | }
105 |
106 | if cfg.Driver == "pppd" && runtime.GOOS == "windows" {
107 | return nil, fmt.Errorf("pppd driver is not supported in Windows")
108 | }
109 |
110 | if !util.StrSliceContains(supportedDrivers, cfg.Driver) {
111 | return nil, fmt.Errorf("%q driver is unsupported, supported drivers are: %q", cfg.Driver, supportedDrivers)
112 | }
113 |
114 | if cfg.ListenDNS == nil {
115 | switch runtime.GOOS {
116 | case "freebsd",
117 | "darwin":
118 | cfg.ListenDNS = defaultBSDDNSListenAddr
119 | default:
120 | cfg.ListenDNS = defaultDNSListenAddr
121 | }
122 | }
123 |
124 | cfg.Path = configPath
125 | cfg.Uid = uid
126 | cfg.Gid = gid
127 |
128 | cfg.Debug = debug
129 |
130 | return cfg, nil
131 | }
132 |
--------------------------------------------------------------------------------
/pkg/client/logger.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | // Logger is an interface representing the Logger struct
14 | type Logger interface {
15 | RequestPrintf(format string, args ...interface{})
16 | ResponsePrintf(format string, args ...interface{})
17 | }
18 |
19 | type logger struct {
20 | RequestID string
21 | }
22 |
23 | func (lg logger) RequestPrintf(format string, args ...interface{}) {
24 | for _, v := range strings.Split(fmt.Sprintf(format, args...), "\n") {
25 | log.Printf("-> %s", v)
26 | }
27 | }
28 |
29 | func (lg logger) ResponsePrintf(format string, args ...interface{}) {
30 | for _, v := range strings.Split(fmt.Sprintf(format, args...), "\n") {
31 | log.Printf("<- %s", v)
32 | }
33 | }
34 |
35 | // noopLogger is a default noop logger satisfies the Logger interface
36 | type noopLogger struct{}
37 |
38 | // Printf is a default noop method
39 | func (noopLogger) RequestPrintf(format string, args ...interface{}) {}
40 |
41 | // Printf is a default noop method
42 | func (noopLogger) ResponsePrintf(format string, args ...interface{}) {}
43 |
44 | // RoundTripper satisfies the http.RoundTripper interface and is used to
45 | // customize the default http client RoundTripper
46 | type RoundTripper struct {
47 | // Default http.RoundTripper
48 | Rt http.RoundTripper
49 | // If Logger is not nil, then RoundTrip method will debug the JSON
50 | // requests and responses
51 | Logger Logger
52 | }
53 |
54 | // formatHeaders converts standard http.Header type to a string with separated headers.
55 | func (rt *RoundTripper) formatHeaders(headers http.Header, separator string) string {
56 | result := make([]string, len(headers))
57 |
58 | i := 0
59 | for header, data := range headers {
60 | result[i] = fmt.Sprintf("%s: %s", header, strings.Join(data, " "))
61 | i++
62 | }
63 |
64 | return strings.Join(result, separator)
65 | }
66 |
67 | // RoundTrip performs a round-trip HTTP request and logs relevant information about it.
68 | func (rt *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
69 | defer func() {
70 | if request.Body != nil {
71 | request.Body.Close()
72 | }
73 | }()
74 |
75 | var err error
76 |
77 | if rt.Logger != nil {
78 | rt.log().RequestPrintf("URL: %s %s", request.Method, request.URL)
79 | rt.log().RequestPrintf("Headers:\n%s", rt.formatHeaders(request.Header, "\n"))
80 |
81 | if request.Body != nil {
82 | request.Body, err = rt.logRequest(request.Body, request.Header.Get("Content-Type"))
83 | if err != nil {
84 | return nil, err
85 | }
86 | }
87 | }
88 |
89 | // this is concurrency safe
90 | ort := rt.Rt
91 | if ort == nil {
92 | return nil, fmt.Errorf("rt RoundTripper is nil, aborting")
93 | }
94 | response, err := ort.RoundTrip(request)
95 |
96 | if response == nil {
97 | if rt.Logger != nil {
98 | rt.log().ResponsePrintf("Connection error, retries exhausted. Aborting")
99 | }
100 | err = fmt.Errorf("connection error, retries exhausted. Aborting. Last error was: %s", err)
101 | return nil, err
102 | }
103 |
104 | if rt.Logger != nil {
105 | rt.log().ResponsePrintf("Code: %d", response.StatusCode)
106 | rt.log().ResponsePrintf("Headers:\n%s", rt.formatHeaders(response.Header, "\n"))
107 |
108 | response.Body, err = rt.logResponse(response.Body, response.Header.Get("Content-Type"))
109 | }
110 |
111 | return response, err
112 | }
113 |
114 | // logRequest will log the HTTP Request details.
115 | // If the body is JSON, it will attempt to be pretty-formatted.
116 | func (rt *RoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
117 | var bs bytes.Buffer
118 | defer original.Close()
119 |
120 | if _, err := io.Copy(&bs, original); err != nil {
121 | return nil, err
122 | }
123 |
124 | rt.log().RequestPrintf("Body: %s", bs.String())
125 |
126 | return ioutil.NopCloser(bytes.NewReader(bs.Bytes())), nil
127 | }
128 |
129 | // logResponse will log the HTTP Response details.
130 | // If the body is JSON, it will attempt to be pretty-formatted.
131 | func (rt *RoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
132 | var bs bytes.Buffer
133 | defer original.Close()
134 |
135 | if _, err := io.Copy(&bs, original); err != nil {
136 | return nil, err
137 | }
138 |
139 | rt.log().ResponsePrintf("Body: %s", bs.String())
140 |
141 | return ioutil.NopCloser(bytes.NewReader(bs.Bytes())), nil
142 | }
143 |
144 | func (rt *RoundTripper) log() Logger {
145 | // this is concurrency safe
146 | l := rt.Logger
147 | if l == nil {
148 | // noop is used, when logger pointer has been set to nil
149 | return &noopLogger{}
150 | }
151 | return l
152 | }
153 |
--------------------------------------------------------------------------------
/pkg/link/pppd.go:
--------------------------------------------------------------------------------
1 | package link
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/hex"
7 | "fmt"
8 | "io"
9 | "log"
10 | "os/exec"
11 | "strings"
12 | "syscall"
13 |
14 | "github.com/kayrus/gof5/pkg/util"
15 |
16 | "github.com/fatih/color"
17 | "github.com/hpcloud/tail"
18 | "github.com/zaninime/go-hdlc"
19 | "golang.org/x/net/ipv4"
20 | )
21 |
22 | // TODO: handle "fatal read pppd: read /dev/ptmx: input/output error"
23 | // TODO: speed test vs native
24 |
25 | func (l *vpnLink) decodeHDLC(buf []byte, src string) {
26 | tmp := bytes.NewBuffer(buf)
27 | frame, err := hdlc.NewDecoder(tmp).ReadFrame()
28 | if err != nil {
29 | log.Printf("fatal decode HDLC frame from %s: %s", src, err)
30 | return
31 | /*
32 | l.ErrChan <- fmt.Errorf("fatal decode HDLC frame from %s: %s", source, err)
33 | return
34 | */
35 | }
36 | log.Printf("Decoded %t prefix HDLC frame from %s:\n%s", frame.HasAddressCtrlPrefix, src, hex.Dump(frame.Payload))
37 | h, err := ipv4.ParseHeader(frame.Payload[:])
38 | if err != nil {
39 | log.Printf("fatal to parse TCP header from %s: %s", src, err)
40 | return
41 | /*
42 | l.ErrChan <- fmt.Errorf("fatal to parse TCP header: %s", err)
43 | return
44 | */
45 | }
46 | log.Printf("TCP: %s", h)
47 | }
48 |
49 | // http->tun
50 | func (l *vpnLink) PppdHTTPToTun(pppd io.WriteCloser) {
51 | buf := make([]byte, bufferSize)
52 | for {
53 | select {
54 | case <-l.TunDown:
55 | return
56 | default:
57 | rn, err := l.HTTPConn.Read(buf)
58 | if err != nil {
59 | if err != io.EOF {
60 | l.ErrChan <- fmt.Errorf("fatal read http: %s", err)
61 | }
62 | return
63 | }
64 | if l.debug {
65 | l.decodeHDLC(buf[:rn], "http")
66 | log.Printf("Read %d bytes from http:\n%s", rn, hex.Dump(buf[:rn]))
67 | }
68 | wn, err := pppd.Write(buf[:rn])
69 | if err != nil {
70 | l.ErrChan <- fmt.Errorf("fatal write to pppd: %s", err)
71 | return
72 | }
73 | if l.debug {
74 | log.Printf("Sent %d bytes to pppd", wn)
75 | }
76 | }
77 | }
78 | }
79 |
80 | // tun->http
81 | func (l *vpnLink) PppdTunToHTTP(pppd io.ReadCloser) {
82 | buf := make([]byte, bufferSize)
83 | for {
84 | select {
85 | case <-l.TunDown:
86 | return
87 | default:
88 | rn, err := pppd.Read(buf)
89 | if err != nil {
90 | if err != io.EOF {
91 | l.ErrChan <- fmt.Errorf("fatal read pppd: %s", err)
92 | }
93 | return
94 | }
95 | if l.debug {
96 | log.Printf("Read %d bytes from pppd:\n%s", rn, hex.Dump(buf[:rn]))
97 | l.decodeHDLC(buf[:rn], "pppd")
98 | }
99 | wn, err := l.HTTPConn.Write(buf[:rn])
100 | if err != nil {
101 | l.ErrChan <- fmt.Errorf("fatal write to http: %s", err)
102 | return
103 | }
104 | if l.debug {
105 | log.Printf("Sent %d bytes to http", wn)
106 | }
107 | }
108 | }
109 | }
110 |
111 | // monitor the the ppp/pppd child process status
112 | func (l *vpnLink) CatchPPPDTermination(cmd *exec.Cmd) {
113 | defer close(l.PppdErrChan)
114 | if err := cmd.Wait(); err != nil {
115 | l.PppdErrChan <- fmt.Errorf("%s process %v", cmd.Path, err)
116 | return
117 | }
118 | }
119 |
120 | // gracefully stop the ppp/pppd child
121 | func (l *vpnLink) StopPPPDChild(cmd *exec.Cmd) {
122 | if cmd != nil && cmd.Process != nil {
123 | cmd.Process.Signal(syscall.SIGTERM)
124 | <-l.PppdErrChan
125 | }
126 | }
127 |
128 | // pppd log parser
129 | func (l *vpnLink) PppdLogParser(stderr io.Reader) {
130 | scanner := bufio.NewScanner(stderr)
131 | for scanner.Scan() {
132 | str := scanner.Text()
133 | if strings.Contains(str, "Using interface") {
134 | if v := strings.FieldsFunc(str, util.SplitFunc); len(v) > 0 {
135 | l.name = v[len(v)-1]
136 | }
137 | }
138 | if strings.Contains(str, "remote IP address") {
139 | close(l.pppUp)
140 | }
141 | colorlog.Print(color.HiGreenString(str))
142 | }
143 | }
144 |
145 | // freebsd ppp log parser
146 | // TODO: talk directly via pppctl
147 | // /etc/ppp/ppp.conf should have `set server /var/run/ppp "" 0177`
148 | func (l *vpnLink) PppLogParser() {
149 | t, err := tail.TailFile("/var/log/ppp.log", tail.Config{
150 | Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
151 | Follow: true,
152 | Logger: tail.DiscardingLogger,
153 | })
154 | if err != nil {
155 | l.ErrChan <- fmt.Errorf("failed to read ppp log: %s", err)
156 | return
157 | }
158 | for line := range t.Lines {
159 | str := line.Text
160 | // strip syslog prefix
161 | if v := strings.SplitN(str, ": ", 2); len(v) == 2 {
162 | str = v[1]
163 | }
164 | if strings.Contains(str, "Using interface") {
165 | if v := strings.FieldsFunc(str, util.SplitFunc); len(v) > 0 {
166 | l.name = v[len(v)-1]
167 | }
168 | }
169 | if strings.Contains(str, "IPCP: myaddr") {
170 | close(l.pppUp)
171 | }
172 | colorlog.Print(color.HiGreenString(str))
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gof5
2 |
3 | ## Requirements
4 |
5 | * an application must be executed under a privileged user
6 |
7 | ## Linux
8 |
9 | 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.
10 | You need to adjust the binary capabilities:
11 |
12 | ```sh
13 | $ sudo setcap cap_net_admin,cap_net_bind_service+ep /path/to/binary/gof5
14 | ```
15 |
16 | For systemd-resolved you need to adjust PolicyKit Local Authority config, e.g. in Ubuntu:
17 |
18 | ```sh
19 | $ cd gof5 # changedir to gof5 github repo
20 | $ sudo cp org.freedesktop.resolve1.pkla /var/lib/polkit-1/localauthority/50-local.d/org.freedesktop.resolve1.pkla
21 | $ sudo systemctl restart polkit.service
22 | ```
23 |
24 | ### Per user capabilities
25 |
26 | If you want to have more granular restrictions to run gof5, you can allow only particular users to run it.
27 |
28 | First of all add an entry before the `none *` in a `/etc/security/capability.conf` file:
29 |
30 | ```
31 | cap_net_admin,cap_net_bind_service %username%
32 | ```
33 |
34 | where a `%username%` is a name of the user, which should get inherited `CAP_NET_ADMIN` and `CAP_NET_BIND_SERVICE` capabilities.
35 |
36 | Adjust the binary flags to have inherited capabilities only:
37 |
38 | ```
39 | $ sudo setcap cap_net_admin,cap_net_bind_service+i /path/to/binary/gof5
40 | ```
41 |
42 | Check user's capabilities:
43 |
44 | ```
45 | $ sudo -u %username% capsh --print | awk '/Current/{print $NF}'
46 | cap_net_bind_service,cap_net_admin+i
47 | ```
48 |
49 | gof5 should be executed using sudo even if you already logged in as this user:
50 |
51 | ```
52 | $ sudo -u %username% /path/to/binary/gof5
53 | ```
54 |
55 | ## MacOS
56 |
57 | On MacOS run the command below to avoid a `cannot be opened because the developer cannot be verified` warning:
58 |
59 | ```sh
60 | xattr -d com.apple.quarantine ./path/to/gof5_darwin
61 | ```
62 |
63 | ## Windows
64 |
65 | Windows version doesn't support `pppd` driver.
66 |
67 | ## ChromeOS
68 |
69 | Developer mode should be enabled, since gof5 requires root privileges.
70 | The binary should be placed inside the `/usr/share/oem` directory. Home directory in ChromeOS doesn't allow to have executables.
71 | You need to restart shill with an option in order to allow tun interface creation: `sudo restart shill BLOCKED_DEVICES=tun0`.
72 | Use the the `driver: pppd` config option if you don't want to restart shill.
73 |
74 | ## HOWTO
75 |
76 | ### Build from source
77 |
78 | ```sh
79 | $ make # gmake in freebsd or mingw make for windows
80 | # or build inside docker (linux version only)
81 | $ make docker
82 | ```
83 |
84 | ### Run
85 |
86 | ```sh
87 | # download the latest release
88 | $ sudo gof5 --server server --username username --password token
89 | ```
90 |
91 | 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:
92 |
93 | ```js
94 | document.cookie.match(/MRHSession=(.*?); /)[1]
95 | ```
96 |
97 | Then specify it as an argument:
98 |
99 | ```sh
100 | $ sudo gof5 --server server --session sessionID
101 | ```
102 |
103 | 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`).
104 |
105 | Use `--close-session` flag to terminate an HTTPS VPN session on exit. Next startup will require a valid username/password.
106 |
107 | Use `--select` to choose a VPN server from the list, known to a current server.
108 |
109 | Use `--profile-index` to define a custom F5 VPN profile index.
110 |
111 | ### CA certificate and TLS keypair
112 |
113 | Use options below to specify custom TLS parameters:
114 |
115 | * `--ca-cert` - path to a custom CA certificate
116 | * `--cert` - path to a user TLS certificate
117 | * `--key` - path to a user TLS key
118 |
119 | ## Configuration
120 |
121 | You can define an extra `~/.gof5/config.yaml` file with contents:
122 |
123 | ```yaml
124 | # DNS proxy listen address, defaults to 127.0.0.245
125 | # In BSD defaults to 127.0.0.1
126 | # listenDNS: 127.0.0.1
127 | # rewrite /etc/resolv.conf instead of renaming
128 | # Linux only, required in cases when /etc/resolv.conf cannot be renamed
129 | rewriteResolv: false
130 | # experimental DTLSv1.2 support
131 | # F5 BIG-IP server should have enabled DTLSv1.2 support
132 | dtls: false
133 | # TLS certificate check
134 | insecureTLS: false
135 | # Enable IPv6
136 | ipv6: false
137 | # driver specifies which tunnel driver to use.
138 | # supported values are: wireguard or pppd.
139 | # wireguard is default.
140 | # pppd requires a pppd or ppp (in FreeBSD) binary
141 | driver: wireguard
142 | # When pppd driver is used, you can specify a list of extra pppd arguments
143 | PPPdArgs: []
144 | # disableDNS allows to completely disable DNS handling,
145 | # i.e. don't alter system DNS (e.g. /etc/resolv.conf) at all
146 | disableDNS: false
147 | # TLS renegotiation support as defined in tls.RenegotiationSupport, disabled by default
148 | renegotiation: RenegotiateNever
149 | # A list of DNS zones to be resolved by VPN DNS servers
150 | # When empty, every DNS query will be resolved by VPN DNS servers
151 | dns:
152 | - .corp.int.
153 | - .corp.
154 | # for reverse DNS lookup
155 | - .in-addr.arpa.
156 | # override DNS servers, provided by a VPN server profile
157 | overrideDNS:
158 | - 8.8.8.8
159 | # override DNS search suffix, provided by a VPN server profile
160 | overrideDNSSuffix:
161 | - my.corp
162 | # A list of subnets to be routed via VPN
163 | # When not set, the routes pushed from F5 will be used
164 | # Use "routes: []", if you don't want gof5 to manage routes at all
165 | routes:
166 | - 1.2.3.4
167 | - 1.2.3.5/32
168 | ```
169 |
--------------------------------------------------------------------------------
/pkg/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "net/http/cookiejar"
11 | "net/url"
12 | "os"
13 | "os/signal"
14 | "runtime"
15 | "syscall"
16 |
17 | "github.com/kayrus/gof5/pkg/config"
18 | "github.com/kayrus/gof5/pkg/cookie"
19 | "github.com/kayrus/gof5/pkg/link"
20 | )
21 |
22 | type Options struct {
23 | config.Config
24 | Server string
25 | Username string
26 | Password string
27 | SessionID string
28 | CACert string
29 | Cert string
30 | Key string
31 | CloseSession bool
32 | Debug bool
33 | Sel bool
34 | Version bool
35 | ProfileIndex int
36 | ProfileName string
37 | Renegotiation tls.RenegotiationSupport
38 | }
39 |
40 | func UrlHandlerF5Vpn(opts *Options, s string) error {
41 | u, err := url.Parse(s)
42 | if err != nil {
43 | return err
44 | }
45 |
46 | if u.Scheme != "f5-vpn" {
47 | return fmt.Errorf("invalid scheme %v expected f5-vpn", u.Scheme)
48 | }
49 |
50 | m, err := url.ParseQuery(u.RawQuery)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | resourceTypes := m["resourcetype"]
56 | resourceNames := m["resourcename"]
57 | if len(resourceTypes) == len(resourceNames) {
58 | for i := range resourceTypes {
59 | if resourceTypes[i] == "network_access" {
60 | opts.ProfileName = resourceNames[i]
61 | break
62 | }
63 | }
64 | }
65 |
66 | opts.Server = m["server"][0]
67 | tokenUrl := fmt.Sprintf("%s://%s:%s/vdesk/get_sessid_for_token.php3", m["protocol"][0], opts.Server, m["port"][0])
68 | request, err := http.NewRequest(http.MethodGet, tokenUrl, nil)
69 | if err != nil {
70 | return err
71 | }
72 | otc := m["otc"]
73 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
74 | request.Header.Add("X-Access-Session-Token", otc[len(otc)-1])
75 |
76 | response, err := http.DefaultClient.Do(request)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | opts.SessionID = response.Header.Get("X-Access-Session-ID")
82 | return nil
83 | }
84 |
85 | func Connect(opts *Options) error {
86 | if opts.Server == "" {
87 | fmt.Print("Enter server address: ")
88 | fmt.Scanln(&opts.Server)
89 | }
90 |
91 | u, err := url.Parse(opts.Server)
92 | if err != nil {
93 | return fmt.Errorf("failed to parse server hostname: %s", err)
94 | }
95 | if u.Scheme != "https" {
96 | u, err = url.Parse(fmt.Sprintf("https://%s", u.Host))
97 | if err != nil {
98 | return fmt.Errorf("failed to parse server hostname: %s", err)
99 | }
100 | }
101 | if u.Host == "" {
102 | u, err = url.Parse(fmt.Sprintf("https://%s", opts.Server))
103 | if err != nil {
104 | return fmt.Errorf("failed to parse server hostname: %s", err)
105 | }
106 | if u.Host == "" {
107 | return fmt.Errorf("failed to parse server hostname: %s", err)
108 | }
109 | }
110 | opts.Server = u.Host
111 |
112 | // read config
113 | cfg, err := config.ReadConfig(opts.Debug)
114 | if err != nil {
115 | return err
116 | }
117 | opts.Config = *cfg
118 |
119 | switch cfg.Renegotiation {
120 | case "RenegotiateOnceAsClient":
121 | opts.Renegotiation = tls.RenegotiateOnceAsClient
122 | case "RenegotiateFreelyAsClient":
123 | opts.Renegotiation = tls.RenegotiateFreelyAsClient
124 | case "RenegotiateNever", "":
125 | opts.Renegotiation = tls.RenegotiateNever
126 | default:
127 | return fmt.Errorf("unknown renegotiation value: '%s'", cfg.Renegotiation)
128 | }
129 |
130 | cookieJar, err := cookiejar.New(nil)
131 | if err != nil {
132 | return fmt.Errorf("failed to create cookie jar: %s", err)
133 | }
134 |
135 | client := &http.Client{Jar: cookieJar}
136 | client.CheckRedirect = checkRedirect(client)
137 |
138 | tlsConf, err := tlsConfig(opts, cfg.InsecureTLS)
139 | if err != nil {
140 | return fmt.Errorf("failed to build TLS config: %v", err)
141 | }
142 | transport := &http.Transport{
143 | TLSClientConfig: tlsConf,
144 | }
145 | if opts.Debug {
146 | client.Transport = &RoundTripper{
147 | Rt: transport,
148 | Logger: &logger{},
149 | }
150 | } else {
151 | client.Transport = transport
152 | }
153 |
154 | // when server select list has been chosen
155 | if opts.Sel {
156 | u, err = getServersList(client, opts.Server)
157 | if err != nil {
158 | return err
159 | }
160 | opts.Server = u.Host
161 | }
162 |
163 | // read cookies
164 | cookie.ReadCookies(client, u, cfg, opts.SessionID)
165 |
166 | if len(client.Jar.Cookies(u)) == 0 {
167 | // need to login
168 | if err := login(client, opts.Server, &opts.Username, &opts.Password); err != nil {
169 | return fmt.Errorf("failed to login: %s", err)
170 | }
171 | } else {
172 | log.Printf("Reusing saved HTTPS VPN session for %s", u.Host)
173 | }
174 |
175 | resp, err := getProfiles(client, opts.Server)
176 | if err != nil {
177 | return fmt.Errorf("failed to get VPN profiles: %s", err)
178 | }
179 |
180 | if resp.StatusCode == 302 {
181 | // need to relogin
182 | _, err = io.Copy(ioutil.Discard, resp.Body)
183 | if err != nil {
184 | return fmt.Errorf("failed to read response body: %s", err)
185 | }
186 | resp.Body.Close()
187 |
188 | if err := login(client, opts.Server, &opts.Username, &opts.Password); err != nil {
189 | return fmt.Errorf("failed to login: %s", err)
190 | }
191 |
192 | // new request
193 | resp, err = getProfiles(client, opts.Server)
194 | if err != nil {
195 | return fmt.Errorf("failed to get VPN profiles: %s", err)
196 | }
197 | }
198 |
199 | if resp.StatusCode != 200 {
200 | return fmt.Errorf("wrong response code on profiles get: %d", resp.StatusCode)
201 | }
202 |
203 | profile, err := parseProfile(resp.Body, opts.ProfileIndex, opts.ProfileName)
204 | if err != nil {
205 | return fmt.Errorf("failed to parse VPN profiles: %s", err)
206 | }
207 |
208 | // read config, returned by F5
209 | cfg.F5Config, err = getConnectionOptions(client, opts, profile)
210 | if err != nil {
211 | return fmt.Errorf("failed to get VPN connection options: %s", err)
212 | }
213 |
214 | // save cookies
215 | if err := cookie.SaveCookies(client, u, cfg); err != nil {
216 | return fmt.Errorf("failed to save cookies: %s", err)
217 | }
218 |
219 | // close HTTPS VPN session
220 | // next VPN connection will require credentials to auth
221 | if opts.CloseSession {
222 | defer closeVPNSession(client, opts.Server)
223 | }
224 |
225 | // TLS
226 | l, err := link.InitConnection(opts.Server, cfg, tlsConf)
227 | if err != nil {
228 | return err
229 | }
230 | defer l.HTTPConn.Close()
231 |
232 | cmd := link.Cmd(cfg)
233 |
234 | termChan := make(chan os.Signal, 1)
235 | signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE, syscall.SIGHUP)
236 |
237 | // set routes and DNS after the PPP/TUN is up
238 | go l.WaitAndConfig(cfg)
239 |
240 | // 1. stop ppp/pppd child at the very end
241 | defer l.StopPPPDChild(cmd)
242 | // 0. restore the config first
243 | defer l.RestoreConfig(cfg)
244 |
245 | if cfg.Driver == "pppd" {
246 | if runtime.GOOS == "freebsd" {
247 | // ppp log parser
248 | go l.PppLogParser()
249 | } else {
250 | /*
251 | // read file descriptor 3
252 | stderr, w, err := os.Pipe()
253 | cmd.ExtraFiles = []*os.File{w}
254 | */
255 | stderr, err := cmd.StderrPipe()
256 | if err != nil {
257 | return fmt.Errorf("cannot allocate stderr pipe: %s", err)
258 | }
259 | // pppd log parser
260 | go l.PppdLogParser(stderr)
261 | }
262 |
263 | stdin, err := cmd.StdinPipe()
264 | if err != nil {
265 | return fmt.Errorf("cannot allocate stdin pipe: %s", err)
266 | }
267 | stdout, err := cmd.StdoutPipe()
268 | if err != nil {
269 | return fmt.Errorf("cannot allocate stdout pipe: %s", err)
270 | }
271 |
272 | err = cmd.Start()
273 | if err != nil {
274 | return fmt.Errorf("failed to start pppd: %s", err)
275 | }
276 |
277 | // catch ppp/pppd child termination
278 | go l.CatchPPPDTermination(cmd)
279 |
280 | // pppd http->tun go routine
281 | go l.PppdHTTPToTun(stdin)
282 |
283 | // pppd tun->http go routine
284 | go l.PppdTunToHTTP(stdout)
285 | } else {
286 | // http->tun go routine
287 | go l.HttpToTun()
288 |
289 | // tun->http go routine
290 | go l.TunToHTTP()
291 | }
292 |
293 | select {
294 | case sig := <-termChan:
295 | log.Printf("received %s signal, exiting", sig)
296 | case err = <-l.ErrChan:
297 | // error received
298 | case err = <-l.PppdErrChan:
299 | // ppp/pppd child error received
300 | }
301 |
302 | // notify tun readers and writes to stop
303 | close(l.TunDown)
304 |
305 | return err
306 | }
307 |
--------------------------------------------------------------------------------
/pkg/link/link.go:
--------------------------------------------------------------------------------
1 | package link
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "encoding/base64"
7 | "fmt"
8 | "io"
9 | "log"
10 | "math/rand"
11 | "net"
12 | "net/http"
13 | "runtime"
14 | "sync"
15 | "time"
16 |
17 | "github.com/kayrus/gof5/pkg/config"
18 | "github.com/kayrus/gof5/pkg/dns"
19 |
20 | "github.com/fatih/color"
21 | "github.com/kayrus/tuncfg/resolv"
22 | "github.com/kayrus/tuncfg/route"
23 | "github.com/kayrus/tuncfg/tun"
24 | "github.com/pion/dtls/v2"
25 | )
26 |
27 | const (
28 | // TUN MTU should not be bigger than buffer size
29 | bufferSize = 1500
30 | userAgentVPN = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0; F5 Networks Client)"
31 | )
32 |
33 | var colorlog = log.New(color.Error, "", log.LstdFlags)
34 |
35 | type vpnLink struct {
36 | sync.Mutex
37 | HTTPConn io.ReadWriteCloser
38 | ErrChan chan error
39 | TunDown chan struct{}
40 | PppdErrChan chan error
41 | iface io.ReadWriteCloser
42 | name string
43 | // pppUp is used to wait for the PPP handshake (wireguard only)
44 | pppUp chan struct{}
45 | // tunUp is used to wait for the TUN interface (wireguard and pppd)
46 | tunUp chan struct{}
47 | serverIPs []net.IP
48 | localIPv4 net.IP
49 | serverIPv4 net.IP
50 | localIPv6 net.IP
51 | serverIPv6 net.IP
52 | mtu []byte
53 | mtuInt uint16
54 | debug bool
55 | routeHandler *route.Handler
56 | resolvHandler *resolv.Handler
57 | }
58 |
59 | func randomHostname(n int) []byte {
60 | var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
61 |
62 | rand.Seed(time.Now().UnixNano())
63 |
64 | b := make([]byte, n)
65 | for i := range b {
66 | b[i] = letters[rand.Intn(len(letters))]
67 | }
68 | return b
69 | }
70 |
71 | // init a TLS connection
72 | func InitConnection(server string, cfg *config.Config, tlsConfig *tls.Config) (*vpnLink, error) {
73 | getURL := fmt.Sprintf("https://%s/myvpn?sess=%s&hostname=%s&hdlc_framing=%s&ipv4=%s&ipv6=%s&Z=%s",
74 | server,
75 | cfg.F5Config.Object.SessionID,
76 | base64.StdEncoding.EncodeToString(randomHostname(8)),
77 | config.Bool(cfg.Driver == "pppd"),
78 | cfg.F5Config.Object.IPv4,
79 | config.Bool(cfg.IPv6 && bool(cfg.F5Config.Object.IPv6)),
80 | cfg.F5Config.Object.UrZ,
81 | )
82 |
83 | serverIPs, err := net.LookupIP(server)
84 | if err != nil || len(serverIPs) == 0 {
85 | return nil, fmt.Errorf("failed to resolve %s: %s", server, err)
86 | }
87 |
88 | // define link channels
89 | l := &vpnLink{
90 | ErrChan: make(chan error, 1),
91 | TunDown: make(chan struct{}, 1),
92 | PppdErrChan: make(chan error, 1),
93 | serverIPs: serverIPs,
94 | pppUp: make(chan struct{}, 1),
95 | tunUp: make(chan struct{}, 1),
96 | debug: cfg.Debug,
97 | }
98 |
99 | if cfg.DTLS && cfg.F5Config.Object.TunnelDTLS {
100 | s := fmt.Sprintf("%s:%s", server, cfg.F5Config.Object.TunnelPortDTLS)
101 | log.Printf("Connecting to %s using DTLS", s)
102 | addr, err := net.ResolveUDPAddr("udp", s)
103 | if err != nil {
104 | return nil, fmt.Errorf("failed to resolve UDP address: %s", err)
105 | }
106 | conf := &dtls.Config{
107 | RootCAs: tlsConfig.RootCAs,
108 | Certificates: tlsConfig.Certificates,
109 | InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
110 | ServerName: server,
111 | }
112 | l.HTTPConn, err = dtls.Dial("udp", addr, conf)
113 | if err != nil {
114 | return nil, fmt.Errorf("failed to dial %s:%s: %s", server, cfg.F5Config.Object.TunnelPortDTLS, err)
115 | }
116 | } else {
117 | l.HTTPConn, err = tls.Dial("tcp", fmt.Sprintf("%s:443", server), tlsConfig)
118 | if err != nil {
119 | return nil, fmt.Errorf("failed to dial %s:443: %s", server, err)
120 | }
121 | }
122 |
123 | req, err := http.NewRequest("GET", getURL, nil)
124 | if err != nil {
125 | return nil, fmt.Errorf("failed to create VPN session request: %s", err)
126 | }
127 | req.Header.Set("User-Agent", userAgentVPN)
128 | err = req.Write(l.HTTPConn)
129 | if err != nil {
130 | return nil, fmt.Errorf("failed to send VPN session request: %s", err)
131 | }
132 |
133 | if l.debug {
134 | log.Printf("URL: %s", getURL)
135 | }
136 |
137 | resp, err := http.ReadResponse(bufio.NewReader(l.HTTPConn), nil)
138 | if err != nil {
139 | return nil, fmt.Errorf("failed to get initial VPN connection response: %s", err)
140 | }
141 | resp.Body.Close()
142 |
143 | l.localIPv4 = net.ParseIP(resp.Header.Get("X-VPN-client-IP"))
144 | l.serverIPv4 = net.ParseIP(resp.Header.Get("X-VPN-server-IP"))
145 | l.localIPv6 = net.ParseIP(resp.Header.Get("X-VPN-client-IPv6"))
146 | l.serverIPv6 = net.ParseIP(resp.Header.Get("X-VPN-server-IPv6"))
147 |
148 | if l.debug {
149 | log.Printf("Client IP: %s", l.localIPv4)
150 | log.Printf("Server IP: %s", l.serverIPv4)
151 | if l.localIPv6 != nil {
152 | log.Printf("Client IPv6: %s", l.localIPv6)
153 | }
154 | if l.localIPv6 != nil {
155 | log.Printf("Server IPv6: %s", l.serverIPv6)
156 | }
157 | }
158 |
159 | return l, nil
160 | }
161 |
162 | func (l *vpnLink) createTunDevice() error {
163 | if l.mtuInt+tun.Offset > bufferSize {
164 | return fmt.Errorf("MTU exceeds the %d buffer limit", bufferSize)
165 | }
166 |
167 | log.Printf("Using wireguard module to create tunnel")
168 | ifname := ""
169 | switch runtime.GOOS {
170 | case "darwin":
171 | ifname = "utun"
172 | case "windows":
173 | ifname = "gof5"
174 | }
175 |
176 | local := &net.IPNet{
177 | IP: l.localIPv4,
178 | Mask: net.CIDRMask(32, 32),
179 | }
180 | gw := &net.IPNet{
181 | IP: l.serverIPv4,
182 | Mask: net.CIDRMask(32, 32),
183 | }
184 | tunDev, err := tun.OpenTunDevice(local, gw, ifname, int(l.mtuInt))
185 | if err != nil {
186 | return fmt.Errorf("failed to create an interface: %s", err)
187 | }
188 | l.name, err = tunDev.Name()
189 | if err != nil {
190 | if e := tunDev.Close(); e != nil {
191 | log.Printf("error closing interface: %v", e)
192 | }
193 | return fmt.Errorf("failed to get an interface name: %s", err)
194 | }
195 |
196 | log.Printf("Created %s interface", l.name)
197 | l.iface = &tun.Tunnel{NativeTun: tunDev}
198 |
199 | // can now process the traffic
200 | close(l.tunUp)
201 |
202 | return nil
203 | }
204 |
205 | func (l *vpnLink) configureDNS(cfg *config.Config) error {
206 | var err error
207 | // this is used only in linux/freebsd to store /etc/resolv.conf backup
208 | resolv.AppName = "gof5"
209 |
210 | dnsSuffixes := cfg.F5Config.Object.DNSSuffix
211 | var dnsServers []net.IP
212 | if len(cfg.DNS) == 0 {
213 | // route everything through VPN gatewy
214 | dnsServers = cfg.F5Config.Object.DNS
215 | } else {
216 | // route only configured suffixes via local DNS proxy
217 | dnsServers = []net.IP{cfg.ListenDNS}
218 | }
219 |
220 | // define DNS servers, provided by F5
221 | l.resolvHandler, err = resolv.New(l.name, dnsServers, dnsSuffixes, cfg.RewriteResolv)
222 | if err != nil {
223 | return err
224 | }
225 |
226 | if cfg.DisableDNS {
227 | // TODO: this is a hack to get real DNS servers, need to be fixed in "tuncfg"
228 | l.resolvHandler.IsResolve()
229 | // no further configuration is required
230 | // get current DNS setting and exit
231 | return nil
232 | }
233 |
234 | if len(cfg.DNS) > 0 && !l.resolvHandler.IsResolve() {
235 | // combine local network search with VPN gateway search
236 | dnsSuffixes = l.resolvHandler.GetOriginalSuffixes()
237 | existingSuffixes := make(map[string]bool)
238 | for _, existingSuffix := range dnsSuffixes {
239 | existingSuffixes[existingSuffix] = true
240 | }
241 |
242 | for _, newSuffix := range cfg.F5Config.Object.DNSSuffix {
243 | if !existingSuffixes[newSuffix] {
244 | dnsSuffixes = append(dnsSuffixes, newSuffix)
245 | }
246 | }
247 | l.resolvHandler.SetSuffixes(dnsSuffixes)
248 | }
249 |
250 | if l.resolvHandler.IsResolve() {
251 | // resolve daemon will route necessary domains through VPN gatewy
252 | log.Printf("Detected systemd-resolved")
253 | l.resolvHandler.SetDNSServers(cfg.F5Config.Object.DNS)
254 | if len(cfg.DNS) > 0 {
255 | log.Printf("Forwarding %q DNS requests to %q", cfg.DNS, cfg.F5Config.Object.DNS)
256 | l.resolvHandler.SetDNSDomains(cfg.DNS)
257 | log.Printf("Default DNS servers: %q", l.resolvHandler.GetOriginalDNS())
258 | } else {
259 | // route all DNS queries via VPN
260 | log.Printf("Forwarding all DNS requests to %q", cfg.F5Config.Object.DNS)
261 | l.resolvHandler.SetDNSDomains([]string{"."})
262 | }
263 | }
264 |
265 | // set DNS and additionally detect original DNS servers, e.g. when NetworkManager is used
266 | err = l.resolvHandler.Set()
267 | if err != nil {
268 | return err
269 | }
270 |
271 | if !l.resolvHandler.IsResolve() {
272 | if len(cfg.DNS) == 0 {
273 | log.Printf("Forwarding all DNS requests to %q", cfg.F5Config.Object.DNS)
274 | return nil
275 | }
276 | cfg.DNSServers = l.resolvHandler.GetOriginalDNS()
277 | log.Printf("Serving DNS proxy on %s:53", cfg.ListenDNS)
278 | log.Printf("Forwarding %q DNS requests to %q", cfg.DNS, cfg.F5Config.Object.DNS)
279 | log.Printf("Default DNS servers: %q", cfg.DNSServers)
280 | dns.Start(cfg, l.ErrChan, l.TunDown)
281 | }
282 |
283 | return nil
284 | }
285 |
286 | // wait for pppd and config DNS and routes
287 | func (l *vpnLink) WaitAndConfig(cfg *config.Config) {
288 | // wait for ppp handshake completed
289 | <-l.pppUp
290 |
291 | l.Lock()
292 | defer l.Unlock()
293 |
294 | var err error
295 |
296 | if cfg.Driver != "pppd" {
297 | // create TUN
298 | err = l.createTunDevice()
299 | if err != nil {
300 | l.ErrChan <- err
301 | return
302 | }
303 | defer func() {
304 | if err != nil && l.iface != nil {
305 | // destroy interface on error
306 | if e := l.iface.Close(); e != nil {
307 | log.Printf("error closing interface: %v", e)
308 | }
309 | }
310 | }()
311 | }
312 |
313 | err = l.configureDNS(cfg)
314 | if err != nil {
315 | l.ErrChan <- err
316 | return
317 | }
318 |
319 | // set routes
320 | log.Printf("Setting routes on %s interface", l.name)
321 |
322 | // set custom routes
323 | routes := cfg.Routes
324 | if routes == nil {
325 | log.Printf("Applying routes, pushed from F5 VPN server")
326 | routes = cfg.F5Config.Object.Routes
327 | }
328 |
329 | // exclude F5 gateway IPs
330 | for _, dst := range l.serverIPs {
331 | // exclude only ipv4
332 | if v := dst.To4(); v != nil {
333 | local := &net.IPNet{
334 | IP: v,
335 | Mask: net.CIDRMask(32, 32),
336 | }
337 | routes.RemoveNet(local)
338 | }
339 | }
340 |
341 | // exclude local DNS servers, when they are not located inside the LAN
342 | for _, v := range l.resolvHandler.GetOriginalDNS() {
343 | localDNS := &net.IPNet{
344 | IP: v,
345 | Mask: net.CIDRMask(32, 32),
346 | }
347 | routes.RemoveNet(localDNS)
348 | }
349 |
350 | var gw net.IP
351 | if runtime.GOOS == "windows" {
352 | // windows requires both gateway and interface name
353 | gw = l.serverIPv4
354 | }
355 |
356 | l.routeHandler, err = route.New(l.name, routes.GetNetworks(), gw, 0)
357 | if err != nil {
358 | l.ErrChan <- err
359 | return
360 | }
361 | l.routeHandler.Add()
362 |
363 | colorlog.Print(color.HiGreenString("Connection established"))
364 | }
365 |
366 | // restore config
367 | func (l *vpnLink) RestoreConfig(cfg *config.Config) {
368 | l.Lock()
369 | defer l.Unlock()
370 |
371 | if l.routeHandler != nil {
372 | log.Printf("Removing routes from %s interface", l.name)
373 | l.routeHandler.Del()
374 | }
375 |
376 | if !cfg.DisableDNS {
377 | if l.resolvHandler != nil {
378 | log.Printf("Restoring DNS settings")
379 | l.resolvHandler.Restore()
380 | }
381 | }
382 |
383 | if cfg.Driver != "pppd" {
384 | if l.iface != nil {
385 | err := l.iface.Close()
386 | if err != nil {
387 | log.Printf("error closing interface: %v", err)
388 | }
389 | }
390 | }
391 | }
392 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/pkg/client/http.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "bytes"
5 | "crypto/hmac"
6 | "crypto/md5"
7 | "crypto/tls"
8 | "crypto/x509"
9 | "encoding/base64"
10 | "encoding/hex"
11 | "encoding/xml"
12 | "fmt"
13 | "io"
14 | "io/ioutil"
15 | "log"
16 | "net/http"
17 | "net/http/cookiejar"
18 | "net/url"
19 | "os"
20 | "regexp"
21 | "strings"
22 |
23 | "github.com/kayrus/gof5/pkg/config"
24 |
25 | "github.com/howeyc/gopass"
26 | "github.com/manifoldco/promptui"
27 | "github.com/mitchellh/go-homedir"
28 | )
29 |
30 | const (
31 | userAgent = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1a2pre) Gecko/2008073000 Shredder/3.0a2pre ThunderBrowse/3.2.1.8"
32 | 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"
33 | )
34 |
35 | func tlsConfig(opts *Options, insecure bool) (*tls.Config, error) {
36 | config := &tls.Config{
37 | InsecureSkipVerify: insecure,
38 | Renegotiation: opts.Renegotiation,
39 | }
40 |
41 | if opts.CACert != "" {
42 | caCert, err := readFile(opts.CACert)
43 | if err != nil {
44 | return nil, err
45 | }
46 | config.RootCAs = x509.NewCertPool()
47 | config.RootCAs.AppendCertsFromPEM(caCert)
48 | }
49 |
50 | if opts.Cert != "" && opts.Key != "" {
51 | crt, err := readFile(opts.Cert)
52 | if err != nil {
53 | return nil, err
54 | }
55 | key, err := readFile(opts.Key)
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | cert, err := tls.X509KeyPair(crt, key)
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | config.Certificates = []tls.Certificate{cert}
66 | }
67 |
68 | return config, nil
69 | }
70 |
71 | func readFile(path string) ([]byte, error) {
72 | if len(path) == 0 {
73 | return nil, nil
74 | }
75 |
76 | if path[0] == '~' {
77 | var err error
78 | path, err = homedir.Expand(path)
79 | if err != nil {
80 | return nil, err
81 | }
82 | }
83 |
84 | if _, err := os.Stat(path); err != nil {
85 | return nil, err
86 | }
87 |
88 | content, err := ioutil.ReadFile(path)
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | return bytes.TrimSpace(content), nil
94 | }
95 |
96 | func checkRedirect(c *http.Client) func(*http.Request, []*http.Request) error {
97 | return func(req *http.Request, via []*http.Request) error {
98 | if req.URL.Path == "/my.logout.php3" || req.URL.Path == "/vdesk/hangup.php3" || req.URL.Query().Get("errorcode") != "" {
99 | // clear cookies
100 | var err error
101 | c.Jar, err = cookiejar.New(nil)
102 | if err != nil {
103 | return fmt.Errorf("failed to create cookie jar: %s", err)
104 | }
105 | return http.ErrUseLastResponse
106 | }
107 | return nil
108 | }
109 | }
110 |
111 | func generateClientData(cData config.ClientData) (string, error) {
112 | info := config.AgentInfo{
113 | Type: "standalone",
114 | Version: "2.0",
115 | Platform: "Linux",
116 | CPU: "x64",
117 | LandingURI: "/",
118 | Hostname: "test",
119 | }
120 |
121 | log.Print(cData.Token)
122 |
123 | data, err := xml.Marshal(info)
124 | if err != nil {
125 | return "", fmt.Errorf("failed to marshal agent info: %s", err)
126 | }
127 |
128 | if info.AppID == "" {
129 | // put appid to the end, when it is empty
130 | r := regexp.MustCompile(">")
131 | data = []byte(r.ReplaceAllString(string(data), ">"))
132 | }
133 |
134 | // signature must be this, when token is "1"
135 | t := "4sY+pQd3zrQ5c2Fl5BwkBg=="
136 |
137 | values := &bytes.Buffer{}
138 | values.WriteString("session=&")
139 | values.WriteString("device_info=" + base64.StdEncoding.EncodeToString(data) + "&")
140 | values.WriteString("agent_result=&")
141 | values.WriteString("token=" + cData.Token)
142 |
143 | // TODO: figure out how to calculate signature
144 | // signature is calculated using cData.Token and UserAgent as a secret key
145 | // 16 bytes, most probably HMAC-MD5
146 | hmacMd5 := hmac.New(md5.New, []byte(cData.Token))
147 |
148 | // write XML into HMAC calc
149 | hmacMd5.Write(values.Bytes())
150 | sig := hmacMd5.Sum(nil)
151 |
152 | log.Printf("HMAC of the values: %x", sig)
153 |
154 | hmacMd5 = hmac.New(md5.New, []byte(cData.Token))
155 |
156 | // write XML into HMAC calc
157 | hmacMd5.Write(data)
158 | sig = hmacMd5.Sum(nil)
159 | log.Printf("HMAC of the data: %x", sig)
160 |
161 | log.Printf("Simple hash of the values: %x", md5.Sum(values.Bytes()))
162 | log.Printf("Simple hash of the data: %x", md5.Sum(data))
163 |
164 | //hmacMd5.Write([]byte(base64.StdEncoding.EncodeToString(data)))
165 |
166 | s, _ := base64.StdEncoding.DecodeString(t)
167 | expected := hex.EncodeToString(s)
168 |
169 | if v := hex.EncodeToString(sig); v != expected {
170 | log.Printf("Signature %q doesn't correspond to %q", v, expected)
171 | }
172 |
173 | // Uncomment this to pass the test
174 | //values.WriteString("signature=" + t)
175 | values.WriteString("&signature=" + base64.StdEncoding.EncodeToString(sig))
176 |
177 | clientData := base64.StdEncoding.EncodeToString(values.Bytes())
178 |
179 | return clientData, nil
180 | }
181 |
182 | func loginSignature(c *http.Client, server string, _, _ *string) error {
183 | log.Printf("Logging in...")
184 | req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/my.logon.php3?outform=xml&client_version=2.0&get_token=1", server), nil)
185 | if err != nil {
186 | return err
187 | }
188 | req.Proto = "HTTP/1.0"
189 | req.Header.Set("User-Agent", androidUserAgent)
190 | resp, err := c.Do(req)
191 | if err != nil {
192 | return err
193 | }
194 |
195 | var cData config.ClientData
196 | dec := xml.NewDecoder(resp.Body)
197 | err = dec.Decode(&cData)
198 | resp.Body.Close()
199 | if err != nil {
200 | return err
201 | }
202 |
203 | clientData, err := generateClientData(cData)
204 | if err != nil {
205 | return err
206 | }
207 |
208 | req, err = http.NewRequest("POST", fmt.Sprintf("https://%s%s", server, cData.RedirectURL), strings.NewReader("client_data="+clientData))
209 | if err != nil {
210 | return err
211 | }
212 | req.Header.Set("User-Agent", androidUserAgent)
213 | req.Header.Set("Pragma", "no-cache")
214 | req.Header.Set("Cache-Control", "no-cache")
215 | req.Header.Set("Upgrade-Insecure-Requests", "1")
216 | req.Header.Set("Origin", "null")
217 | 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")
218 | req.Header.Set("content-type", "application/x-www-form-urlencoded")
219 | req.Header.Set("X-Requested-With", "com.f5.edge.client_ics")
220 | req.Header.Set("Sec-Fetch-Site", "none")
221 | req.Header.Set("Sec-Fetch-Mode", "navigate")
222 | req.Header.Set("Sec-Fetch-User", "?1")
223 | req.Header.Set("Sec-Fetch-Dest", "document")
224 | req.Header.Set("Accept-Encoding", "gzip, deflate")
225 | req.Header.Set("Accept-Language", "en-US;q=0.9,en;q=0.8")
226 |
227 | resp, err = c.Do(req)
228 | if err != nil {
229 | return err
230 | }
231 | defer resp.Body.Close()
232 |
233 | if resp.StatusCode == 302 {
234 | return fmt.Errorf("login failed")
235 | }
236 |
237 | _, err = ioutil.ReadAll(resp.Body)
238 | if err != nil {
239 | return err
240 | }
241 |
242 | return nil
243 | }
244 |
245 | func login(c *http.Client, server string, username, password *string) error {
246 | if *username == "" {
247 | fmt.Print("Enter VPN username: ")
248 | fmt.Scanln(username)
249 | }
250 | if *password == "" {
251 | fmt.Print("Enter VPN password: ")
252 | v, err := gopass.GetPasswd()
253 | if err != nil {
254 | return fmt.Errorf("failed to read password: %s", err)
255 | }
256 | *password = string(v)
257 | }
258 |
259 | log.Printf("Logging in...")
260 | req, err := http.NewRequest("GET", fmt.Sprintf("https://%s", server), nil)
261 | if err != nil {
262 | return err
263 | }
264 | req.Proto = "HTTP/1.0"
265 | req.Header.Set("User-Agent", userAgent)
266 | resp, err := c.Do(req)
267 | if err != nil {
268 | return err
269 | }
270 | _, err = ioutil.ReadAll(resp.Body)
271 | if err != nil {
272 | return err
273 | }
274 | resp.Body.Close()
275 |
276 | data := url.Values{}
277 | data.Set("username", *username)
278 | data.Add("password", *password)
279 | data.Add("vhost", "standard")
280 | req, err = http.NewRequest("POST", fmt.Sprintf("https://%s/my.policy?outform=xml", server), strings.NewReader(data.Encode()))
281 | if err != nil {
282 | return err
283 | }
284 | req.Header.Set("Referer", fmt.Sprintf("https://%s/my.policy", server))
285 | req.Header.Set("User-Agent", userAgent)
286 | resp, err = c.Do(req)
287 | if err != nil {
288 | return err
289 | }
290 | body, err := ioutil.ReadAll(resp.Body)
291 | if err != nil {
292 | return err
293 | }
294 | resp.Body.Close()
295 |
296 | /*
297 | if resp.StatusCode == 302 && resp.Header.Get("Location") == "/my.policy" {
298 | return nil
299 | }
300 | */
301 |
302 | // TODO: parse response 302 location and error code
303 | if resp.StatusCode == 302 || bytes.Contains(body, []byte("Session Expired/Timeout")) || bytes.Contains(body, []byte("The username or password is not correct")) {
304 | return fmt.Errorf("wrong credentials")
305 | }
306 |
307 | return nil
308 | }
309 |
310 | func parseProfile(reader io.ReadCloser, profileIndex int, profileName string) (string, error) {
311 | var profiles config.Profiles
312 | dec := xml.NewDecoder(reader)
313 | err := dec.Decode(&profiles)
314 | reader.Close()
315 | if err != nil {
316 | return "", fmt.Errorf("failed to unmarshal a response: %s", err)
317 | }
318 |
319 | if profiles.Type == "VPN" {
320 | prfls := make([]string, len(profiles.Favorites))
321 | for i, p := range profiles.Favorites {
322 | if profileName != "" && profileName == p.Name {
323 | profileIndex = i
324 | }
325 | prfls[i] = fmt.Sprintf("%d:%s", i, p.Name)
326 | }
327 | log.Printf("Found F5 VPN profiles: %q", prfls)
328 |
329 | if profileIndex >= len(profiles.Favorites) {
330 | return "", fmt.Errorf("profile %q index is out of range", profileIndex)
331 | }
332 | log.Printf("Using %q F5 VPN profile", profiles.Favorites[profileIndex].Name)
333 | return profiles.Favorites[profileIndex].Params, nil
334 | }
335 |
336 | return "", fmt.Errorf("VPN profile was not found")
337 | }
338 |
339 | func getProfiles(c *http.Client, server string) (*http.Response, error) {
340 | req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/vdesk/vpn/index.php3?outform=xml&client_version=2.0", server), nil)
341 | if err != nil {
342 | return nil, fmt.Errorf("failed to build a request: %s", err)
343 | }
344 | req.Header.Set("User-Agent", userAgent)
345 | return c.Do(req)
346 | }
347 |
348 | func getConnectionOptions(c *http.Client, opts *Options, profile string) (*config.Favorite, error) {
349 | req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/vdesk/vpn/connect.php3?%s&outform=xml&client_version=2.0", opts.Server, profile), nil)
350 | if err != nil {
351 | return nil, fmt.Errorf("failed to build a request: %s", err)
352 | }
353 | req.Header.Set("User-Agent", userAgent)
354 | resp, err := c.Do(req)
355 |
356 | if err != nil {
357 | log.Printf("Failed to read a request: %s", err)
358 | log.Printf("Override link DNS values from config")
359 | return &config.Favorite{
360 | Object: config.Object{
361 | SessionID: opts.SessionID,
362 | DNS: opts.Config.OverrideDNS,
363 | DNSSuffix: opts.Config.OverrideDNSSuffix,
364 | },
365 | }, nil
366 | }
367 |
368 | // parse profile
369 | var favorite config.Favorite
370 | dec := xml.NewDecoder(resp.Body)
371 | err = dec.Decode(&favorite)
372 | resp.Body.Close()
373 | if err != nil {
374 | return nil, fmt.Errorf("failed to unmarshal a response: %s", err)
375 | }
376 |
377 | // override link options
378 | if favorite.Object.SessionID == "" {
379 | favorite.Object.SessionID = opts.SessionID
380 | }
381 | if len(opts.Config.OverrideDNS) > 0 {
382 | favorite.Object.DNS = opts.Config.OverrideDNS
383 | }
384 | if len(opts.Config.OverrideDNSSuffix) > 0 {
385 | favorite.Object.DNSSuffix = opts.Config.OverrideDNSSuffix
386 | }
387 |
388 | return &favorite, nil
389 | }
390 |
391 | func closeVPNSession(c *http.Client, server string) {
392 | // close session
393 | r, err := http.NewRequest("GET", fmt.Sprintf("https://%s/vdesk/hangup.php3?hangup_error=1", server), nil)
394 | if err != nil {
395 | log.Printf("Failed to create a request to close the VPN session %s", err)
396 | }
397 | resp, err := c.Do(r)
398 | if err != nil {
399 | log.Printf("Failed to close the VPN session %s", err)
400 | }
401 | defer resp.Body.Close()
402 | }
403 |
404 | func getServersList(c *http.Client, server string) (*url.URL, error) {
405 | r, err := http.NewRequest("GET", fmt.Sprintf("https://%s/pre/config.php", server), nil)
406 | if err != nil {
407 | return nil, fmt.Errorf("failed to create a request to get servers list: %s", err)
408 | }
409 | resp, err := c.Do(r)
410 | if err != nil {
411 | return nil, fmt.Errorf("failed to request servers list: %s", err)
412 | }
413 |
414 | var s config.PreConfigProfile
415 | dec := xml.NewDecoder(resp.Body)
416 | err = dec.Decode(&s)
417 | resp.Body.Close()
418 | if err != nil {
419 | return nil, fmt.Errorf("failed to unmarshal servers list: %s", err)
420 | }
421 |
422 | prompt := promptui.Select{
423 | Label: "Select Server",
424 | Items: s.Servers,
425 | }
426 |
427 | i, _, err := prompt.Run()
428 | if err != nil {
429 | return nil, fmt.Errorf("prompt failed: %s", err)
430 | }
431 |
432 | u, err := url.Parse(s.Servers[i].Address)
433 | if err != nil {
434 | return nil, fmt.Errorf("failed to parse server hostname: %s", err)
435 | }
436 |
437 | // if scheme is not set, assume https
438 | if u.Scheme == "" {
439 | u, err = url.Parse("https://" + s.Servers[i].Address)
440 | if err != nil {
441 | return nil, fmt.Errorf("failed to parse server hostname: %s", err)
442 | }
443 | }
444 |
445 | return u, nil
446 | }
447 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0=
2 | github.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4=
3 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
5 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
13 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
14 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
15 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
16 | github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
17 | github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
18 | github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=
19 | github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
20 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
21 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
22 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
23 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
24 | github.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382 h1:FGitiUIfcFXN472O3leZ5+n1w97D6+R3xJZxRQRy9es=
25 | github.com/kayrus/tuncfg v0.0.0-20211029100448-15eab7b00382/go.mod h1:bU3N4PUqV+NW8pCT4gOS5Z7R1rqEq50q3vfP9hRhNj0=
26 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
28 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
29 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
31 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
32 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
33 | github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
34 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
35 | github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
36 | github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
37 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
38 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
39 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
40 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
41 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
42 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
43 | github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
44 | github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
45 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
46 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
47 | github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
48 | github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
49 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
50 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
51 | github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
52 | github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
53 | github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
54 | github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57 | github.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162 h1:2zlAtlrum6lg2lMiUWznq04fDudBDajMFl94Zyis67Y=
58 | github.com/sigurn/crc16 v0.0.0-20160107003519-da416fad5162/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
59 | github.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f h1:fKe0QdNJw68NO8iUdbC+jlwaA7/pA8sw0caZkpeXFTc=
60 | github.com/sigurn/utils v0.0.0-20151230205143-f19e41f79f8f/go.mod h1:VRI4lXkrUH5Cygl6mbG1BRUfMMoT2o8BkrtBDUAm+GU=
61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
62 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
63 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
64 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
65 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
66 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
67 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
68 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
69 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
70 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
71 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
72 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
73 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
74 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
75 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
76 | github.com/zaninime/go-hdlc v1.1.1 h1:L0NBRiv49mSsCC+oSEmTbAcUntr8nseJpC+6pwYkBZ0=
77 | github.com/zaninime/go-hdlc v1.1.1/go.mod h1:u/pMQOkSk+AucNZiuoil1ZKuO510qk8jn1JRyO7GR5w=
78 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
79 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
80 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
81 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
82 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
83 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
84 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
85 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
86 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
87 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
88 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
89 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
90 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
91 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
92 | golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
93 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
94 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
95 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
96 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
97 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
98 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
99 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
100 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
101 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
102 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
103 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
104 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
105 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
106 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
107 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
108 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
109 | golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
110 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
111 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
112 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
113 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
114 | golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
115 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
116 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
117 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
118 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
119 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
120 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
121 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
123 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
124 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
125 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
126 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
127 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
128 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
129 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
130 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
131 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
132 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
133 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
134 | golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=
135 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
136 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
137 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
138 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
139 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
140 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
141 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
142 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
143 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
144 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
145 | golang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71 h1:xANEpH9Q0hSSf/ogUZZg9yPBxo2x9Js+7LZoHI/EJRE=
146 | golang.zx2c4.com/wireguard v0.0.0-20211028114750-eb6302c7eb71/go.mod h1:RTjaYEQboNk7+2qfPGBotaMEh/5HIvmPZ6DIe10lTqI=
147 | golang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a h1:4+nkXW+gJ+wq7ZqAspB4hAQymenjGwgN4O/IHn+kTm0=
148 | golang.zx2c4.com/wireguard/windows v0.5.2-0.20211028141252-9fe93eaf9c4a/go.mod h1:EC9wJeih/xJQBE9kSNihR8I0WmojSTQRQMsCLMpzqYY=
149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
150 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
151 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
152 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
153 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
154 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
155 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
156 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
157 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
158 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
159 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
160 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
161 | kernel.org/pub/linux/libs/security/libcap/cap v1.2.48 h1:gW8VCEsPUwAp0/cW8CN2zfoqvz0+ijagsH2x+O2KlMM=
162 | kernel.org/pub/linux/libs/security/libcap/cap v1.2.48/go.mod h1:cs/AYPYd93hM59y4VPzpn4FP5TFgFoCcKtzlb0LM1c8=
163 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.48 h1:5Oh8T4MP1+3KV2SvCBkCeGd97g7QHWMkTS7SrEme2bA=
164 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.48/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
165 |
--------------------------------------------------------------------------------
/pkg/link/f5.go:
--------------------------------------------------------------------------------
1 | package link
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "encoding/hex"
7 | "fmt"
8 | "io"
9 | "log"
10 | "net"
11 |
12 | "golang.org/x/net/ipv4"
13 | "golang.org/x/net/ipv6"
14 | )
15 |
16 | func readBuf(buf, sep []byte) []byte {
17 | n := bytes.Index(buf, sep)
18 | if n == 0 {
19 | return buf[len(sep):]
20 | }
21 | return nil
22 | }
23 |
24 | var (
25 | ppp = []byte{0xff, 0x03}
26 | pppLCP = []byte{0xc0, 0x21}
27 | pppIPCP = []byte{0x80, 0x21}
28 | pppIPv6CP = []byte{0x80, 0x57}
29 | // LCP auth
30 | mtuRequest = []byte{0x00, 0x18}
31 | // Link-Discriminator
32 | terminate = []byte{0x00, 0x17}
33 | // No network protocols
34 | noProtocols = []byte{0x00, 0x20}
35 | // Session-Timeout
36 | timeout = []byte{0x00, 0x13}
37 | //
38 | mtuResponse = []byte{0x00, 0x12}
39 | protoRej = []byte{0x00, 0x2c}
40 | mtuHeader = []byte{0x01, 0x04}
41 | mtuSize = 2
42 | ipv6type = []byte{0x00, 0x0e}
43 | ipv4type = []byte{0x00, 0x0a}
44 | v4 = []byte{0x06}
45 | v6 = []byte{0x0a}
46 | pfc = []byte{0x07, 0x02}
47 | acfc = []byte{0x08, 0x02}
48 | accm = []byte{0x02, 0x06, 0x00, 0x00, 0x00, 0x00}
49 | magicHeader = []byte{0x05, 0x06}
50 | magicSize = 4
51 | ipv4header = []byte{0x21}
52 | ipv6header = []byte{0x57}
53 | //
54 | confRequest = []byte{0x01}
55 | confAck = []byte{0x02}
56 | confNack = []byte{0x03}
57 | confRej = []byte{0x04}
58 | confTermReq = []byte{0x05}
59 | protoReject = []byte{0x08}
60 | echoReq = []byte{0x09}
61 | echoRep = []byte{0x0a}
62 | )
63 |
64 | func bytesToIPv4(bytes []byte) net.IP {
65 | return net.IP(append(bytes[:0:0], bytes...))
66 | }
67 |
68 | func bytesToIPv6(bytes []byte) net.IP {
69 | return net.IP(append([]byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, append(bytes[:0:0], bytes...)...))
70 | }
71 |
72 | func processPPP(l *vpnLink, buf []byte, dstBuf *bytes.Buffer) error {
73 | // process ipv4 traffic
74 | if v := readBuf(buf, ipv4header); v != nil {
75 | if l.debug {
76 | log.Printf("Read parsed ipv4 %d bytes from http:\n%s", len(v), hex.Dump(v))
77 | header, _ := ipv4.ParseHeader(v)
78 | log.Printf("ipv4 from http: %s", header)
79 | }
80 |
81 | wn, err := l.iface.Write(v)
82 | if err != nil {
83 | return fmt.Errorf("fatal write to tun: %s", err)
84 | }
85 | if l.debug {
86 | log.Printf("Sent %d bytes to tun", wn)
87 | }
88 | return nil
89 | }
90 |
91 | // process ipv6 traffic
92 | if v := readBuf(buf, ipv6header); v != nil {
93 | if l.debug {
94 | log.Printf("Read parsed ipv6 %d bytes from http:\n%s", len(v), hex.Dump(v))
95 | header, _ := ipv6.ParseHeader(v)
96 | log.Printf("ipv6 from http: %s", header)
97 | }
98 |
99 | wn, err := l.iface.Write(v)
100 | if err != nil {
101 | return fmt.Errorf("fatal write to tun: %s", err)
102 | }
103 | if l.debug {
104 | log.Printf("Sent %d bytes to tun", wn)
105 | }
106 | return nil
107 | }
108 |
109 | // TODO: support IPv4 only
110 | if v := readBuf(buf, pppIPCP); v != nil {
111 | if v := readBuf(v, confRequest); v != nil {
112 | id := v[0]
113 | if v := readBuf(v[1:], ipv4type); v != nil {
114 | id2 := v[0]
115 | if v := readBuf(v[1:], v4); v != nil {
116 | l.serverIPv4 = bytesToIPv4(v)
117 | log.Printf("id: %d, id2: %d, Remote IPv4 requested: %s", id, id2, l.serverIPv4)
118 |
119 | doResp := &bytes.Buffer{}
120 | doResp.Write(ppp)
121 | doResp.Write(pppIPCP)
122 | //
123 | doResp.Write(confAck)
124 | doResp.WriteByte(id)
125 | doResp.Write(ipv4type)
126 | doResp.WriteByte(id2)
127 | doResp.Write(v4)
128 | doResp.Write(v)
129 |
130 | err := toF5(l, doResp.Bytes(), dstBuf)
131 | if err != nil {
132 | return err
133 | }
134 |
135 | doResp = &bytes.Buffer{}
136 | doResp.Write(ppp)
137 | doResp.Write(pppIPCP)
138 | //
139 | doResp.Write(confRequest)
140 | doResp.WriteByte(id)
141 | doResp.Write(ipv4type)
142 | doResp.WriteByte(id2)
143 | doResp.Write(v4)
144 | for i := 0; i < 4; i++ {
145 | doResp.WriteByte(0)
146 | }
147 |
148 | return toF5(l, doResp.Bytes(), dstBuf)
149 | }
150 | }
151 | }
152 | if v := readBuf(v, confAck); v != nil {
153 | id := v[0]
154 | if v := readBuf(v[1:], ipv4type); v != nil {
155 | id2 := v[0]
156 | if v := readBuf(v[1:], v4); v != nil {
157 | l.localIPv4 = bytesToIPv4(v)
158 | log.Printf("id: %d, id2: %d, Local IPv4 acknowledged: %s", id, id2, l.localIPv4)
159 |
160 | // connection established
161 | close(l.pppUp)
162 |
163 | return nil
164 | }
165 | }
166 | }
167 | if v := readBuf(v, confNack); v != nil {
168 | id := v[0]
169 | if v := readBuf(v[1:], ipv4type); v != nil {
170 | id2 := v[0]
171 | if v := readBuf(v[1:], v4); v != nil {
172 | log.Printf("id: %d, id2: %d, Local IPv4 not acknowledged: %s", id, id2, bytesToIPv4(v))
173 |
174 | doResp := &bytes.Buffer{}
175 | doResp.Write(ppp)
176 | doResp.Write(pppIPCP)
177 | //
178 | doResp.Write(confRequest)
179 | doResp.WriteByte(id)
180 | doResp.Write(ipv4type)
181 | doResp.WriteByte(id2)
182 | doResp.Write(v4)
183 | doResp.Write(v)
184 |
185 | return toF5(l, doResp.Bytes(), dstBuf)
186 | }
187 | }
188 | }
189 | }
190 |
191 | // pppIPv6CP
192 | if v := readBuf(buf, pppIPv6CP); v != nil {
193 | if v := readBuf(v, confRequest); v != nil {
194 | id := v[0]
195 | if v := readBuf(v[1:], ipv6type); v != nil {
196 | id2 := v[0]
197 | if v := readBuf(v[1:], v6); v != nil {
198 | l.serverIPv6 = bytesToIPv6(v)
199 | log.Printf("id: %d, id2: %d, Remote IPv6 requested: %s", id, id2, l.serverIPv6)
200 |
201 | doResp := &bytes.Buffer{}
202 | doResp.Write(ppp)
203 | doResp.Write(pppIPv6CP)
204 | //
205 | doResp.Write(confAck)
206 | doResp.WriteByte(id)
207 | doResp.Write(ipv6type)
208 | doResp.WriteByte(id2)
209 | doResp.Write(v6)
210 | doResp.Write(v)
211 |
212 | err := toF5(l, doResp.Bytes(), dstBuf)
213 | if err != nil {
214 | return err
215 | }
216 |
217 | doResp = &bytes.Buffer{}
218 | doResp.Write(ppp)
219 | doResp.Write(pppIPv6CP)
220 | //
221 | doResp.Write(confRequest)
222 | doResp.WriteByte(id)
223 | doResp.Write(ipv6type)
224 | doResp.WriteByte(id2)
225 | doResp.Write(v6)
226 | for i := 0; i < 8; i++ {
227 | doResp.WriteByte(0)
228 | }
229 |
230 | return toF5(l, doResp.Bytes(), dstBuf)
231 | }
232 | }
233 | }
234 | if v := readBuf(v, confAck); v != nil {
235 | id := v[0]
236 | if v := readBuf(v[1:], ipv6type); v != nil {
237 | id2 := v[0]
238 | if v := readBuf(v[1:], v6); v != nil {
239 | l.localIPv6 = bytesToIPv6(v)
240 | log.Printf("id: %d, id2: %d, Local IPv6 acknowledged: %s", id, id2, l.localIPv6)
241 |
242 | return nil
243 | }
244 | }
245 | }
246 | if v := readBuf(v, confNack); v != nil {
247 | id := v[0]
248 | if v := readBuf(v[1:], ipv6type); v != nil {
249 | id2 := v[0]
250 | if v := readBuf(v[1:], v6); v != nil {
251 | log.Printf("id: %d, id2: %d, Local IPv6 not acknowledged: %s", id, id2, bytesToIPv6(v))
252 |
253 | doResp := &bytes.Buffer{}
254 | doResp.Write(ppp)
255 | doResp.Write(pppIPv6CP)
256 | //
257 | doResp.Write(confRequest)
258 | doResp.WriteByte(id)
259 | doResp.Write(ipv6type)
260 | doResp.WriteByte(id2)
261 | doResp.Write(v6)
262 | doResp.Write(v)
263 |
264 | return toF5(l, doResp.Bytes(), dstBuf)
265 | }
266 | }
267 | }
268 | }
269 |
270 | // it is PPP header
271 | if v := readBuf(buf, ppp); v != nil {
272 | // it is pppLCP
273 | if v := readBuf(v, pppLCP); v != nil {
274 | if v := readBuf(v, confTermReq); v != nil {
275 | id := v[0]
276 | if v := readBuf(v[1:], terminate); v != nil {
277 | return fmt.Errorf("id: %d, Link terminated with: %s", id, v)
278 | }
279 | if v := readBuf(v[1:], timeout); v != nil {
280 | return fmt.Errorf("id: %d, Link timed out with: %s", id, v)
281 | }
282 | if v := readBuf(v[1:], noProtocols); v != nil {
283 | return fmt.Errorf("id: %d, Link terminated with: %s", id, v)
284 | }
285 | }
286 | if v := readBuf(v, echoReq); v != nil {
287 | id := v[0]
288 | if l.debug {
289 | log.Printf("id: %d, echo", id)
290 | }
291 | // live pings
292 | doResp := &bytes.Buffer{}
293 | doResp.Write(ppp)
294 | doResp.Write(pppLCP)
295 | //
296 | doResp.Write(echoRep)
297 | doResp.WriteByte(id)
298 | doResp.Write(v[1:])
299 |
300 | return toF5(l, doResp.Bytes(), dstBuf)
301 | }
302 | if v := readBuf(v, protoReject); v != nil {
303 | id := v[0]
304 | if v := readBuf(v[1:], protoRej); v != nil {
305 | log.Printf("id: %d, Protocol reject:\n%s", id, hex.Dump(v))
306 | return nil
307 | }
308 | }
309 | // it is pppLCP
310 | if v := readBuf(v, confRequest); v != nil {
311 | id := v[0]
312 | // configuration requested
313 | if v := readBuf(v[1:], mtuRequest); v != nil {
314 | // MTU request
315 | if v := readBuf(v, mtuHeader); v != nil {
316 | // set MTU
317 | t := v[:mtuSize]
318 | l.mtu = append(t[:0:0], t...)
319 | l.mtuInt = binary.BigEndian.Uint16(l.mtu)
320 | log.Printf("MTU: %d", l.mtuInt)
321 | if v := readBuf(v[mtuSize:], accm); v != nil {
322 | if v := readBuf(v, magicHeader); v != nil {
323 | magic := v[:magicSize]
324 | log.Printf("Magic: %x", magic)
325 | log.Printf("PFC: %x", v[magicSize:magicSize+len(pfc)])
326 | log.Printf("ACFC: %x", v[magicSize+len(pfc):])
327 |
328 | doResp := &bytes.Buffer{}
329 | doResp.Write(ppp)
330 | doResp.Write(pppLCP)
331 | //
332 | doResp.Write(confRequest)
333 | doResp.WriteByte(id)
334 | doResp.Write(ipv6type)
335 | doResp.Write(accm)
336 | doResp.Write(pfc)
337 | doResp.Write(acfc)
338 |
339 | err := toF5(l, doResp.Bytes(), dstBuf)
340 | if err != nil {
341 | return err
342 | }
343 |
344 | doResp = &bytes.Buffer{}
345 | doResp.Write(ppp)
346 | doResp.Write(pppLCP)
347 | //
348 | doResp.Write(confRej)
349 | //doResp.Write(confRequest)
350 | doResp.WriteByte(id)
351 | doResp.Write(ipv4type)
352 | doResp.Write(magicHeader)
353 | doResp.Write(magic)
354 |
355 | return toF5(l, doResp.Bytes(), dstBuf)
356 | }
357 | return fmt.Errorf("wrong magic header: %x", v)
358 | }
359 | return fmt.Errorf("wrong ACCM: %x", v)
360 | }
361 | }
362 | if v := readBuf(v[1:], mtuResponse); v != nil {
363 | if v := readBuf(v, mtuHeader); v != nil {
364 | if v := readBuf(v, l.mtu); v != nil {
365 | if v := readBuf(v, accm); v != nil {
366 | if v := readBuf(v, pfc); v != nil {
367 | if v := readBuf(v, acfc); v != nil {
368 | log.Printf("id: %d, MTU accepted", id)
369 |
370 | doResp := &bytes.Buffer{}
371 | doResp.Write(ppp)
372 | doResp.Write(pppLCP)
373 | //
374 | doResp.Write(confAck)
375 | doResp.WriteByte(id)
376 | doResp.Write(mtuResponse)
377 | doResp.Write(mtuHeader)
378 | doResp.Write(l.mtu)
379 | doResp.WriteByte(id)
380 | doResp.Write(v4)
381 | for i := 0; i < 4; i++ {
382 | doResp.WriteByte(0)
383 | }
384 | doResp.Write(pfc)
385 | doResp.Write(acfc)
386 |
387 | return toF5(l, doResp.Bytes(), dstBuf)
388 | }
389 | }
390 | }
391 | }
392 | }
393 | }
394 | }
395 | // do set
396 | if v := readBuf(v, confAck); v != nil {
397 | // required settings
398 | id := v[0]
399 | if v := readBuf(v[1:], ipv6type); v != nil {
400 | if v := readBuf(v, accm); v != nil {
401 | if v := readBuf(v, pfc); v != nil {
402 | if v := readBuf(v, acfc); v != nil {
403 | log.Printf("id: %d, IPV6 accepted", id)
404 | return nil
405 | }
406 | }
407 | }
408 | }
409 | }
410 | if v := readBuf(v, confNack); v != nil {
411 | id := v[0]
412 | if v := readBuf(v[1:], mtuRequest); v != nil {
413 | if v := readBuf(v, mtuHeader); v != nil {
414 | if v := readBuf(v, l.mtu); v != nil {
415 | return fmt.Errorf("id: %d, MTU not acknowledged:\n%s", id, hex.Dump(v))
416 | }
417 | }
418 | }
419 | if v := readBuf(v[1:], ipv4type); v != nil {
420 | if v := readBuf(v, magicHeader); v != nil {
421 | return fmt.Errorf("id: %d, IPv4 not acknowledged:\n%s", id, hex.Dump(v))
422 | }
423 | }
424 | }
425 | }
426 | }
427 |
428 | return fmt.Errorf("unknown PPP data:\n%s", hex.Dump(buf))
429 | }
430 |
431 | func fromF5(l *vpnLink, dstBuf *bytes.Buffer) error {
432 | // read the F5 packet header
433 | buf := make([]byte, 2)
434 | _, err := io.ReadFull(l.HTTPConn, buf)
435 | if err != nil {
436 | return fmt.Errorf("failed to read F5 packet header: %s", err)
437 | }
438 | if !(buf[0] == 0xf5 && buf[1] == 00) {
439 | return fmt.Errorf("incorrect F5 header: %x", buf)
440 | }
441 |
442 | // read the F5 packet size
443 | var pkglen uint16
444 | err = binary.Read(l.HTTPConn, binary.BigEndian, &pkglen)
445 | if err != nil {
446 | return fmt.Errorf("failed to read F5 packet size: %s", err)
447 | }
448 |
449 | // read the packet
450 | buf = make([]byte, pkglen)
451 | n, err := io.ReadFull(l.HTTPConn, buf)
452 | if err != nil {
453 | return fmt.Errorf("failed to read F5 packet of the %d size: %s", pkglen, err)
454 | }
455 | if n != int(pkglen) {
456 | return fmt.Errorf("incorrect F5 packet size: %d, expected: %d", n, pkglen)
457 | }
458 |
459 | // process the packet
460 | return processPPP(l, buf, dstBuf)
461 | }
462 |
463 | // Decode F5 packet
464 | // http->tun
465 | func (l *vpnLink) HttpToTun() {
466 | dstBuf := &bytes.Buffer{}
467 | for {
468 | select {
469 | case <-l.TunDown:
470 | return
471 | default:
472 | err := fromF5(l, dstBuf)
473 | if err != nil {
474 | l.ErrChan <- err
475 | return
476 | }
477 | }
478 | }
479 | }
480 |
481 | func toF5(l *vpnLink, buf []byte, dst *bytes.Buffer) error {
482 | // TODO: move buffer initialization into tunToHTTP
483 | // probably a buffered pipe would be nicer
484 | length := len(buf)
485 | if length == 0 {
486 | return fmt.Errorf("cannot encapsulate zero packet")
487 | }
488 |
489 | defer dst.Reset()
490 |
491 | // TODO: check packet header length (ipv4.HeaderLen, ipv6.HeaderLen)
492 | switch buf[0] >> 4 {
493 | case ipv4.Version:
494 | length += len(ipv4header)
495 | case ipv6.Version:
496 | length += len(ipv6header)
497 | }
498 |
499 | _, err := dst.Write([]byte{0xf5, 0x00})
500 | if err != nil {
501 | return fmt.Errorf("failed to write F5 header: %s", err)
502 | }
503 | err = binary.Write(dst, binary.BigEndian, uint16(length))
504 | if err != nil {
505 | return fmt.Errorf("failed to write F5 header size: %s", err)
506 | }
507 |
508 | switch buf[0] >> 4 {
509 | case ipv4.Version:
510 | _, err = dst.Write(ipv4header)
511 | case ipv6.Version:
512 | _, err = dst.Write(ipv6header)
513 | }
514 | if err != nil {
515 | return fmt.Errorf("failed to write IP header: %s", err)
516 | }
517 |
518 | if l.debug {
519 | log.Printf("Sending from pppd:\n%s", hex.Dump(buf))
520 | }
521 |
522 | _, err = dst.Write(buf)
523 | if err != nil {
524 | return fmt.Errorf("fatal write to http: %s", err)
525 | }
526 | wn, err := io.Copy(l.HTTPConn, dst)
527 | if err != nil {
528 | return fmt.Errorf("fatal write to http: %s", err)
529 | }
530 | if l.debug {
531 | log.Printf("Sent %d bytes to http", wn)
532 | }
533 |
534 | return nil
535 | }
536 |
537 | // Encode into F5 packet
538 | // tun->http
539 | func (l *vpnLink) TunToHTTP() {
540 | buf := make([]byte, bufferSize)
541 | dstBuf := &bytes.Buffer{}
542 | for {
543 | select {
544 | case <-l.TunDown:
545 | return
546 | case <-l.tunUp:
547 | rn, err := l.iface.Read(buf)
548 | if err != nil {
549 | if err != io.EOF {
550 | l.ErrChan <- fmt.Errorf("fatal read tun: %s", err)
551 | }
552 | return
553 | }
554 | if l.debug {
555 | log.Printf("Read %d bytes from tun:\n%s", rn, hex.Dump(buf[:rn]))
556 | header, _ := ipv4.ParseHeader(buf[:rn])
557 | log.Printf("ipv4 from tun: %s", header)
558 | }
559 |
560 | err = toF5(l, buf[:rn], dstBuf)
561 | if err != nil {
562 | l.ErrChan <- err
563 | return
564 | }
565 | }
566 | }
567 | }
568 |
--------------------------------------------------------------------------------
/pkg/config/types.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/xml"
6 | "fmt"
7 | "log"
8 | "net"
9 | "net/url"
10 | "strings"
11 |
12 | "github.com/kayrus/gof5/pkg/util"
13 |
14 | "github.com/IBM/netaddr"
15 | )
16 |
17 | type Config struct {
18 | Debug bool `yaml:"-"`
19 | Driver string `yaml:"driver"`
20 | ListenDNS net.IP `yaml:"-"`
21 | DNS []string `yaml:"dns"`
22 | OverrideDNS []net.IP `yaml:"-"`
23 | OverrideDNSSuffix []string `yaml:"overrideDNSSuffix"`
24 | Routes *netaddr.IPSet `yaml:"-"`
25 | PPPdArgs []string `yaml:"pppdArgs"`
26 | InsecureTLS bool `yaml:"insecureTLS"`
27 | DTLS bool `yaml:"dtls"`
28 | IPv6 bool `yaml:"ipv6"`
29 | // completely disable DNS servers handling
30 | DisableDNS bool `yaml:"disableDNS"`
31 | // rewrite /etc/resolv.conf instead of renaming
32 | // required in ChromeOS, where /etc/resolv.conf cannot be renamed
33 | RewriteResolv bool `yaml:"rewriteResolv"`
34 | // tls regeneration, tls.RenegotiateNever by default
35 | Renegotiation string `yaml:"renegotiation"`
36 | // list of detected local DNS servers
37 | DNSServers []net.IP `yaml:"-"`
38 | // config path
39 | Path string `yaml:"-"`
40 | // current user or sudo user UID
41 | Uid int `yaml:"-"`
42 | // current user or sudo user GID
43 | Gid int `yaml:"-"`
44 | // Config, returned by F5
45 | F5Config *Favorite `yaml:"-"`
46 | }
47 |
48 | func (r *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
49 | type tmp Config
50 | var s struct {
51 | tmp
52 | ListenDNS *string `yaml:"listenDNS"`
53 | Routes []string `yaml:"routes"`
54 | PPPdArgs []string `yaml:"pppdArgs"`
55 | OverrideDNS []string `yaml:"overrideDNS"`
56 | }
57 |
58 | if err := unmarshal(&s.tmp); err != nil {
59 | return err
60 | }
61 |
62 | if err := unmarshal(&s); err != nil {
63 | return err
64 | }
65 |
66 | *r = Config(s.tmp)
67 |
68 | if s.ListenDNS != nil {
69 | r.ListenDNS = net.ParseIP(*s.ListenDNS)
70 | }
71 |
72 | r.Routes = new(netaddr.IPSet)
73 | if s.Routes != nil {
74 | // handle the case, when routes is an empty list
75 | parsedCIDRs, err := parseCIDRs(s.Routes, net.IPv4len)
76 | if err != nil {
77 | return err
78 | }
79 | r.Routes = subnetsToIPSet(parsedCIDRs)
80 | }
81 |
82 | if len(s.OverrideDNS) > 0 {
83 | r.OverrideDNS = processIPs(strings.Join(s.OverrideDNS, " "), net.IPv4len)
84 | }
85 |
86 | // default pppd arguments
87 | r.PPPdArgs = []string{
88 | "logfd", "2",
89 | "noauth",
90 | "nodetach",
91 | "passive",
92 | "ipcp-accept-local",
93 | "ipcp-accept-remote",
94 | "notty", // use default stdin/stdout
95 | "nodefaultroute",
96 | // nocompression
97 | "novj",
98 | "novjccomp",
99 | "noaccomp",
100 | "noccp",
101 | "nopcomp",
102 | "nopredictor1",
103 | "nodeflate", // Protocol-Reject for 'Compression Control Protocol' (0x80fd) received
104 | "nobsdcomp", // Protocol-Reject for 'Compression Control Protocol' (0x80fd) received
105 | }
106 | if len(s.PPPdArgs) > 0 {
107 | // extra pppd args
108 | r.PPPdArgs = append(r.PPPdArgs, s.PPPdArgs...)
109 | }
110 |
111 | return nil
112 | }
113 |
114 | type Favorite struct {
115 | Object Object `xml:"object"`
116 | }
117 |
118 | type Bool bool
119 |
120 | func (b Bool) String() string {
121 | if b {
122 | return "yes"
123 | }
124 | return "no"
125 | }
126 |
127 | func (b Bool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
128 | return e.EncodeElement(b.String(), start)
129 | }
130 |
131 | func strToBool(s string) (Bool, error) {
132 | switch v := strings.ToLower(s); v {
133 | case "yes":
134 | return true, nil
135 | case "no", "":
136 | return false, nil
137 | }
138 | return false, fmt.Errorf("cannot parse boolean: %s", s)
139 | }
140 |
141 | // TODO: unmarshal for bool
142 |
143 | type Object struct {
144 | SessionID string `xml:"Session_ID"`
145 | IPv4 Bool `xml:"IPV4_0"`
146 | IPv6 Bool `xml:"IPV6_0"`
147 | UrZ string `xml:"ur_Z"`
148 | HDLCFraming Bool `xml:"-"`
149 | Host string `xml:"host0"`
150 | Port string `xml:"port0"`
151 | TunnelHost string `xml:"tunnel_host0"`
152 | TunnelPort string `xml:"tunnel_port0"`
153 | Add2Hosts string `xml:"Add2Hosts0"`
154 | DNSRegisterConnection int `xml:"DNSRegisterConnection0"`
155 | DNSUseDNSSuffixForRegistration int `xml:"DNSUseDNSSuffixForRegistration0"`
156 | SplitTunneling int `xml:"SplitTunneling0"`
157 | DNSSPlit string `xml:"DNS_SPLIT0"`
158 | TunnelDTLS bool `xml:"tunnel_dtls"`
159 | TunnelPortDTLS string `xml:"tunnel_port_dtls"`
160 | AllowLocalSubnetAccess bool `xml:"AllowLocalSubnetAccess0"`
161 | AllowLocalDNSServersAccess bool `xml:"AllowLocalDNSServersAccess0"`
162 | AllowLocalDHCPAccess bool `xml:"AllowLocalDHCPAccess0"`
163 | DNS []net.IP `xml:"-"`
164 | DNS6 []net.IP `xml:"-"`
165 | ExcludeSubnets []*net.IPNet `xml:"-"`
166 | Routes *netaddr.IPSet `xml:"-"`
167 | ExcludeSubnets6 []*net.IPNet `xml:"-"`
168 | Routes6 *netaddr.IPSet `xml:"-"`
169 | TrafficControl TrafficControl `xml:"-"`
170 | DNSSuffix []string `xml:"-"`
171 | }
172 |
173 | type TrafficControl struct {
174 | Flow []Flow `xml:"flow"`
175 | }
176 |
177 | type Flow struct {
178 | Name string `xml:"name,attr"`
179 | Rate string `xml:"rate,attr"`
180 | Ceiling string `xml:"ceiling,attr"`
181 | Mode string `xml:"mode,attr"`
182 | Burst string `xml:"burst,attr"`
183 | Type string `xml:"type,attr"`
184 | Via string `xml:"via,attr"`
185 | Filter Filter `xml:"filter"`
186 | }
187 |
188 | type Filter struct {
189 | Proto string `xml:"proto,attr"`
190 | Src string `xml:"src,attr"`
191 | SrcMask string `xml:"src_mask,attr"`
192 | SrcPort string `xml:"src_port,attr"`
193 | Dst string `xml:"dst,attr"`
194 | DstMask string `xml:"dst_mask,attr"`
195 | DstPort string `xml:"dst_port,attr"`
196 | }
197 |
198 | func (o *Object) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
199 | type tmp Object
200 | var s struct {
201 | tmp
202 | DNS string `xml:"DNS0"`
203 | DNS6 string `xml:"DNS6_0"`
204 | ExcludeSubnets string `xml:"ExcludeSubnets0"`
205 | ExcludeSubnets6 string `xml:"ExcludeSubnets6_0"`
206 | TrafficControl string `xml:"TrafficControl0"`
207 | HDLCFraming string `xml:"hdlc_framing"`
208 | DNSSuffix string `xml:"DNSSuffix0"`
209 | }
210 |
211 | err := d.DecodeElement(&s, &start)
212 | if err != nil {
213 | return err
214 | }
215 | *o = Object(s.tmp)
216 |
217 | if v, err := url.QueryUnescape(s.TrafficControl); err != nil {
218 | return fmt.Errorf("failed to unescape %q: %s", s.TrafficControl, err)
219 | } else if v := strings.TrimSpace(v); v != "" {
220 | if err = xml.Unmarshal([]byte(v), &o.TrafficControl); err != nil {
221 | return err
222 | }
223 | }
224 |
225 | o.DNS = processIPs(s.DNS, net.IPv4len)
226 | o.DNS6 = processIPs(s.DNS6, net.IPv6len)
227 | o.ExcludeSubnets = processCIDRs(s.ExcludeSubnets, net.IPv4len)
228 | o.ExcludeSubnets6 = processCIDRs(s.ExcludeSubnets6, net.IPv6len)
229 |
230 | // TODO: support IPv6 routes
231 | o.Routes = inverseCIDRs4(o.ExcludeSubnets)
232 |
233 | o.HDLCFraming, err = strToBool(s.HDLCFraming)
234 | if err != nil {
235 | return err
236 | }
237 |
238 | if v := strings.TrimSpace(s.DNSSuffix); v != "" {
239 | o.DNSSuffix = strings.Split(v, ",")
240 | }
241 |
242 | return nil
243 | }
244 |
245 | type Session struct {
246 | Token string `xml:"token"`
247 | Version string `xml:"version"`
248 | RedirectURL string `xml:"redirect_url"`
249 | MaxClientData string `xml:"max_client_data"`
250 | }
251 |
252 | // Profiles list
253 | type Profiles struct {
254 | Type string `xml:"type,attr"`
255 | Limited string `xml:"limited,attr"`
256 | Favorites []FavoriteItem `xml:"favorite"`
257 | }
258 |
259 | type FavoriteItem struct {
260 | ID string `xml:"id,attr"`
261 | Caption string `xml:"caption"`
262 | Name string `xml:"name"`
263 | Params string `xml:"params"`
264 | }
265 |
266 | type Hostname string
267 |
268 | func (h Hostname) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
269 | return e.EncodeElement(base64.StdEncoding.EncodeToString([]byte(h)), start)
270 | }
271 |
272 | func processIPs(ips string, length int) []net.IP {
273 | if v := strings.FieldsFunc(strings.TrimSpace(ips), util.SplitFunc); len(v) > 0 {
274 | var t []net.IP
275 | for _, v := range v {
276 | v := net.ParseIP(v)
277 | if length == net.IPv4len {
278 | if v.To4() != nil {
279 | t = append(t, v)
280 | }
281 | } else if length == net.IPv6len {
282 | t = append(t, v.To16())
283 | }
284 | }
285 | return t
286 | }
287 | return nil
288 | }
289 |
290 | func parseCIDRs(cidrs []string, length int) ([]*net.IPNet, error) {
291 | t := make([]*net.IPNet, len(cidrs))
292 | for i, v := range cidrs {
293 | var cidr *net.IPNet
294 | var err error
295 |
296 | if ip := net.ParseIP(v); ip != nil {
297 | cidr = &net.IPNet{
298 | IP: ip,
299 | Mask: net.CIDRMask(32, 32),
300 | }
301 | } else {
302 | // parse 1.2.3.4/12 format
303 | _, cidr, err = net.ParseCIDR(v)
304 | if err != nil {
305 | return nil, fmt.Errorf("failed to parse %q cidr: %v", v, err)
306 | }
307 | }
308 | if length == net.IPv4len {
309 | t[i] = &net.IPNet{
310 | IP: cidr.IP.To4(),
311 | Mask: cidr.Mask,
312 | }
313 | } else if length == net.IPv6len {
314 | t[i] = &net.IPNet{
315 | IP: cidr.IP.To16(),
316 | Mask: cidr.Mask,
317 | }
318 | }
319 | }
320 | return t, nil
321 | }
322 |
323 | func processCIDRs(cidrs string, length int) []*net.IPNet {
324 | if v := strings.FieldsFunc(strings.TrimSpace(cidrs), util.SplitFunc); len(v) > 0 {
325 | var t []*net.IPNet
326 | for _, v := range v {
327 | // parse 1.2.3.4/255.255.255.0 format
328 | if v := strings.Split(v, "/"); len(v) == 2 {
329 | ip := net.ParseIP(v[0])
330 | mask := net.ParseIP(v[1])
331 | if ip == nil || mask == nil {
332 | log.Printf("Cannot parse %q CIDR", v)
333 | continue
334 | }
335 | if length == net.IPv4len {
336 | t = append(t, &net.IPNet{
337 | IP: ip.To4(),
338 | Mask: net.IPMask(mask.To4()),
339 | })
340 | } else if length == net.IPv6len {
341 | t = append(t, &net.IPNet{
342 | IP: ip.To16(),
343 | Mask: net.IPMask(mask.To16()),
344 | })
345 | }
346 | continue
347 | }
348 | log.Printf("Cannot parse %q CIDR", v)
349 | }
350 | return t
351 | }
352 | return nil
353 | }
354 |
355 | func subnetsToIPSet(subnets []*net.IPNet) *netaddr.IPSet {
356 | // initialize an empty IPSet
357 | ipSet4 := &netaddr.IPSet{}
358 |
359 | for _, v := range subnets {
360 | ipSet4.InsertNet(v)
361 | }
362 |
363 | // get a routes list
364 | return ipSet4
365 | }
366 |
367 | func inverseCIDRs4(exclude []*net.IPNet) *netaddr.IPSet {
368 | // initialize an empty IPSet
369 | ipSet4 := &netaddr.IPSet{}
370 |
371 | all := &net.IPNet{
372 | IP: net.IPv4zero.To4(),
373 | Mask: net.CIDRMask(0, 32),
374 | }
375 | ipSet4.InsertNet(all)
376 |
377 | // remove reserved addresses (rfc8190)
378 | soft := &net.IPNet{
379 | IP: net.IPv4zero.To4(),
380 | Mask: net.CIDRMask(8, 32),
381 | }
382 | ipSet4.RemoveNet(soft)
383 |
384 | local := &net.IPNet{
385 | IP: net.IPv4(127, 0, 0, 0).To4(),
386 | Mask: net.CIDRMask(8, 32),
387 | }
388 | ipSet4.RemoveNet(local)
389 |
390 | unicast := &net.IPNet{
391 | IP: net.IPv4(169, 254, 0, 0).To4(),
392 | Mask: net.CIDRMask(16, 32),
393 | }
394 | ipSet4.RemoveNet(unicast)
395 |
396 | multicast := &net.IPNet{
397 | IP: net.IPv4(224, 0, 0, 0).To4(),
398 | Mask: net.CIDRMask(4, 32),
399 | }
400 | ipSet4.RemoveNet(multicast)
401 |
402 | for _, v := range exclude {
403 | ipSet4.RemoveNet(v)
404 | }
405 |
406 | // get a routes list
407 | return ipSet4
408 | }
409 |
410 | type AgentInfo struct {
411 | XMLName xml.Name `xml:"agent_info"`
412 | Type string `xml:"type"`
413 | Version string `xml:"version"`
414 | Platform string `xml:"platform"`
415 | CPU string `xml:"cpu"`
416 | JavaScript Bool `xml:"javascript"`
417 | ActiveX Bool `xml:"activex"`
418 | Plugin Bool `xml:"plugin"`
419 | LandingURI string `xml:"landinguri"`
420 | Model string `xml:"model,omitempty"`
421 | PlatformVersion string `xml:"platform_version,omitempty"`
422 | MACAddress string `xml:"mac_address,omitempty"`
423 | UniqueID string `xml:"unique_id,omitempty"`
424 | SerialNumber string `xml:"serial_number,omitempty"`
425 | AppID string `xml:"app_id,omitempty"`
426 | AppVersion string `xml:"app_version,omitempty"`
427 | JailBreak *Bool `xml:"jailbreak,omitempty"`
428 | VPNScope string `xml:"vpn_scope,omitempty"`
429 | VPNStartType string `xml:"vpn_start_type,omitempty"`
430 | LockedMode Bool `xml:"lockedmode"`
431 | VPNTunnelType string `xml:"vpn_tunnel_type,omitempty"`
432 | Hostname Hostname `xml:"hostname"`
433 | BiometricFingerprint *Bool `xml:"biometric_fingerprint,omitempty"`
434 | DevicePasscodeSet *Bool `xml:"device_passcode_set,omitempty"`
435 | }
436 |
437 | type ClientData struct {
438 | XMLName xml.Name `xml:"data"`
439 | Token string `xml:"token"`
440 | Version string `xml:"version"`
441 | RedirectURL string `xml:"redirect_url"`
442 | MaxClientData int `xml:"max_client_data"`
443 | }
444 |
445 | type PreConfigProfile struct {
446 | XMLName xml.Name `xml:"PROFILE"`
447 | Version string `xml:"VERSION,attr"`
448 | Servers []Server `xml:"SERVERS>SITEM"`
449 | Session preConfigSession `xml:"SESSION"`
450 | DNSSuffix []string `xml:"LOCATIONS>CORPORATE>DNSSUFFIX"`
451 | }
452 |
453 | type Server struct {
454 | Address string `xml:"ADDRESS"`
455 | Alias string `xml:"ALIAS"`
456 | }
457 |
458 | type preConfigSession struct {
459 | Limited Bool `xml:"-"`
460 | SaveOnExit Bool `xml:"-"`
461 | SavePasswords Bool `xml:"-"`
462 | ReuseWinlogonCreds Bool `xml:"-"`
463 | ReuseWinlogonSession Bool `xml:"-"`
464 | PasswordPolicy PasswordPolicy `xml:"PASSWORD_POLICY"`
465 | Update Update `xml:"UPDATE"`
466 | }
467 |
468 | func (o *preConfigSession) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
469 | type tmp preConfigSession
470 | var s struct {
471 | tmp
472 | Limited string `xml:"LIMITED,attr"`
473 | SaveOnExit string `xml:"SAVEONEXIT"`
474 | SavePasswords string `xml:"SAVEPASSWORDS"`
475 | ReuseWinlogonCreds string `xml:"REUSEWINLOGONCREDS"`
476 | ReuseWinlogonSession string `xml:"REUSEWINLOGONSESSION"`
477 | }
478 |
479 | err := d.DecodeElement(&s, &start)
480 | if err != nil {
481 | return err
482 | }
483 | *o = preConfigSession(s.tmp)
484 |
485 | o.Limited, err = strToBool(s.Limited)
486 | if err != nil {
487 | return err
488 | }
489 |
490 | o.SaveOnExit, err = strToBool(s.SaveOnExit)
491 | if err != nil {
492 | return err
493 | }
494 |
495 | o.SavePasswords, err = strToBool(s.SavePasswords)
496 | if err != nil {
497 | return err
498 | }
499 |
500 | o.ReuseWinlogonCreds, err = strToBool(s.ReuseWinlogonCreds)
501 | if err != nil {
502 | return err
503 | }
504 |
505 | o.ReuseWinlogonSession, err = strToBool(s.ReuseWinlogonSession)
506 | if err != nil {
507 | return err
508 | }
509 |
510 | return nil
511 | }
512 |
513 | type PasswordPolicy struct {
514 | Mode string `xml:"MODE"`
515 | Timeout int `xml:"TIMEOUT"`
516 | }
517 |
518 | type Update struct {
519 | Mode Bool `xml:"-"`
520 | }
521 |
522 | func (o *Update) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
523 | type tmp Update
524 | var s struct {
525 | tmp
526 | Mode string `xml:"MODE"`
527 | }
528 |
529 | err := d.DecodeElement(&s, &start)
530 | if err != nil {
531 | return err
532 | }
533 | *o = Update(s.tmp)
534 |
535 | o.Mode, err = strToBool(s.Mode)
536 | if err != nil {
537 | return err
538 | }
539 |
540 | return nil
541 | }
542 |
--------------------------------------------------------------------------------