├── .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.com
One
https://f5-2.com
Two
YESNONONODISK240YEScorp.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 | --------------------------------------------------------------------------------