From 010643757e29fc403874ffb5ad6f30464e0fb02d Mon Sep 17 00:00:00 2001 From: brent saner Date: Tue, 17 Dec 2024 17:39:10 -0500 Subject: [PATCH] initial commit --- .gitignore | 49 ++++++ build.sh | 87 ++++++++++ cachedb/_static/cache.schema.sql | 31 ++++ cachedb/consts.go | 15 ++ cachedb/funcs.go | 35 ++++ cachedb/types.go | 27 +++ cmd/gobroke/main.go | 1 + conf/_testdata/test.json | 82 +++++++++ conf/_testdata/test.toml | 289 +++++++++++++++++++++++++++++++ conf/_testdata/test.xml | 68 ++++++++ conf/_testdata/test.yml | 59 +++++++ conf/consts.go | 9 + conf/errs.go | 10 ++ conf/funcs.go | 75 ++++++++ conf/funcs_test.go | 50 ++++++ conf/funcs_tunnel.go | 1 + conf/types.go | 129 ++++++++++++++ go.mod | 27 +++ go.sum | 61 +++++++ tplCmd/funcs_cmd.go | 34 ++++ tplCmd/funcs_templatecmd.go | 73 ++++++++ tplCmd/types.go | 21 +++ tunnelbroker/consts.go | 23 +++ tunnelbroker/funcs.go | 9 + tunnelbroker/types.go | 49 ++++++ version/consts.go | 32 ++++ version/funcs.go | 144 +++++++++++++++ version/funcs_buildinfo.go | 103 +++++++++++ version/types.go | 51 ++++++ 29 files changed, 1644 insertions(+) create mode 100644 .gitignore create mode 100755 build.sh create mode 100644 cachedb/_static/cache.schema.sql create mode 100644 cachedb/consts.go create mode 100644 cachedb/funcs.go create mode 100644 cachedb/types.go create mode 100644 cmd/gobroke/main.go create mode 100644 conf/_testdata/test.json create mode 100644 conf/_testdata/test.toml create mode 100644 conf/_testdata/test.xml create mode 100644 conf/_testdata/test.yml create mode 100644 conf/consts.go create mode 100644 conf/errs.go create mode 100644 conf/funcs.go create mode 100644 conf/funcs_test.go create mode 100644 conf/funcs_tunnel.go create mode 100644 conf/types.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 tplCmd/funcs_cmd.go create mode 100644 tplCmd/funcs_templatecmd.go create mode 100644 tplCmd/types.go create mode 100644 tunnelbroker/consts.go create mode 100644 tunnelbroker/funcs.go create mode 100644 tunnelbroker/types.go create mode 100644 version/consts.go create mode 100644 version/funcs.go create mode 100644 version/funcs_buildinfo.go create mode 100644 version/types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f5fb6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +*.7z +*.bak +*.deb +*.jar +*.rar +*.run +*.sig +*.tar +*.tar.bz2 +*.tar.gz +*.tar.xz +*.tbz +*.tbz2 +*.tgz +*.txz +*.zip +.*.swp +.editix + +# https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +#*.test +test.sh + +# Built binary +bin/* +poc/* +_poc/* + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Example configs. +_exampledata/ + +# Don't include rendered doc +#/README.html + +# Example code. +_demo/ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..9cc516c --- /dev/null +++ b/build.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -e + +# This is not portable. It has bashisms. + +BUILD_TIME="$(date '+%s')" +BUILD_USER="$(whoami)" +BUILD_SUDO_USER="${SUDO_USER}" +BUILD_HOST="$(hostname)" + +# Check to make sure git is available. +if ! command -v git &> /dev/null; +then + echo "Git is not available; automatic version handling unsupported." + echo "You must build by calling 'go build' directly in the respective directories." + exit 0 +fi + +# Check git directory/repository. +if ! git rev-parse --is-inside-work-tree &>/dev/null; +then + echo "Not running inside a git work tree; automatic version handling unsupported/build script unsupported." + echo "You must build by calling 'go build' directly in the respective directories instead." + exit 0 +fi + +# If it has a tag in the path of the current HEAD that matches a version string... +# I wish git describe supported regex. It does not; only globs. Gross. +# If there's a bug anywhere, it's here. +if git describe --tags --abbrev=0 --match "v[0-9]*" HEAD &> /dev/null; +then + # It has a tag we can use. + CURRENT_VER="$(git describe --tags --abbrev=0 --match "v[0-9]*" HEAD)" + COMMITS_SINCE="$(git rev-list --count ${CURRENT_VER}..HEAD)" +else + # No tag available. + CURRENT_VER="" + COMMITS_SINCE="" +fi + +# If it's dirty (staged but not committed or unstaged files)... +if ! git diff-index --quiet HEAD; +then + # It's dirty. + IS_DIRTY="1" +else + # It's clean. + IS_DIRTY="0" +fi + +# Get the commit hash of the *most recent* commit in the path of current HEAD... +CURRENT_HASH="$(git rev-parse --verify HEAD)" +# The same as above, but abbreviated. +CURRENT_SHORT="$(git rev-parse --verify --short HEAD)" + +# Get the module name. +MODPATH="$(sed -n -re 's@^\s*module\s+(.*)(//.*)?$@\1@p' go.mod)" + +# Build the ldflags string. +# BEHOLD! BASH WITCHCRAFT. +LDFLAGS_STR="\ +-X '${MODPATH}/version.sourceControl=git' \ +-X '${MODPATH}/version.version=${CURRENT_VER}' \ +-X '${MODPATH}/version.commitHash=${CURRENT_HASH}' \ +-X '${MODPATH}/version.commitShort=${CURRENT_SHORT}' \ +-X '${MODPATH}/version.numCommitsAfterTag=${COMMITS_SINCE}' \ +-X '${MODPATH}/version.isDirty=${IS_DIRTY}' \ +-X '${MODPATH}/version.buildTime=${BUILD_TIME}' \ +-X '${MODPATH}/version.buildUser=${BUILD_USER}' \ +-X '${MODPATH}/version.buildSudoUser=${BUILD_SUDO_USER}' \ +-X '${MODPATH}/version.buildHost=${BUILD_HOST}'" + +# And finally build. +mkdir -p ./bin/ +export CGO_ENABLED=0 + +cmd="gobroke" +# Linux +echo -n "Building ./bin/${cmd}..." +go build \ + -o "./bin/${cmd}" \ + -ldflags \ + "${LDFLAGS_STR}" \ + cmd/${cmd}/*.go +echo " Done." + +echo "Build complete." diff --git a/cachedb/_static/cache.schema.sql b/cachedb/_static/cache.schema.sql new file mode 100644 index 0000000..36cc313 --- /dev/null +++ b/cachedb/_static/cache.schema.sql @@ -0,0 +1,31 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE tunnels ( + tun_id INTEGER NOT NULL PRIMARY KEY, + cksum_crc32 INTEGER NOT NULL, + "desc" TEXT, + server_v4 TEXT NOT NULL, + current_client_v4 TEXT NOT NULL, + tunnel_server_v6 TEXT NOT NULL, + tunnel_client_v6 TEXT NOT NULL, + prefix_64 TEXT NOT NULL, + prefix_48 TEXT, + rdns_1 TEXT, + rdns_2 TEXT, + rdns_3 TEXT, + rdns_4 TEXT, + rdns_5 TEXT, + created INTEGER NOT NULL, + checked INTEGER NOT NULL, + updated INTEGER +); +CREATE TABLE client_ips ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tun_id INTEGER NOT NULL, + client_ip INTEGER NOT NULL, + when_set INTEGER NOT NULL, when_fetched INTEGER, + CONSTRAINT client_ips_tunnels_FK FOREIGN KEY (tun_id) REFERENCES tunnels(tun_id) ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO sqlite_sequence VALUES('client_ips',0); +COMMIT; +PRAGMA foreign_keys=ON; diff --git a/cachedb/consts.go b/cachedb/consts.go new file mode 100644 index 0000000..a640da5 --- /dev/null +++ b/cachedb/consts.go @@ -0,0 +1,15 @@ +package cachedb + +import ( + _ "embed" +) + +var ( + //go:embed "_static/cache.schema.sql" + schemaBytes []byte +) + +const ( + SelectTunnels string = "" + SelectTunnelById string = "" +) diff --git a/cachedb/funcs.go b/cachedb/funcs.go new file mode 100644 index 0000000..a6e80a8 --- /dev/null +++ b/cachedb/funcs.go @@ -0,0 +1,35 @@ +package cachedb + +import ( + `os` + `path/filepath` + + `r00t2.io/sysutils/paths` +) + +/* + NewCache returns a Cache from path to SQLite file `db` (or ":memory:" for an in-memory one). + + It will be created if it doesn't exist for persistent caches. +*/ +func NewCache(db string) (c *Cache, err error) { + + var exists bool + + switch db { + case ":memory:": + default: + if exists, err = paths.RealPathExists(&db); err != nil { + return + } + if !exists { + if err = os.MkdirAll(filepath.Dir(db), 0700); err != nil { + return + } + if err = os.WriteFile() + } + } + // TODO + + return +} diff --git a/cachedb/types.go b/cachedb/types.go new file mode 100644 index 0000000..1dbc277 --- /dev/null +++ b/cachedb/types.go @@ -0,0 +1,27 @@ +package cachedb + +import ( + `time` + + `github.com/jmoiron/sqlx` + `r00t2.io/gobroke/tunnelbroker` +) + +type Cache struct { + db *sqlx.DB +} + +type TunnelDB struct { + *tunnelbroker.Tunnel + Created time.Time `db:"created"` + Checked time.Time `db:"checked"` + Updated time.Time `db:"updated"` +} + +type ClientIpDB struct { + ID uint64 `db:"id"` + TunID uint64 `db:"tun_id"` + *tunnelbroker.FetchedIP + Set time.Time `db:"when_set"` + Fetched time.Time `db:"when_fetched"` +} diff --git a/cmd/gobroke/main.go b/cmd/gobroke/main.go new file mode 100644 index 0000000..0dd3a1d --- /dev/null +++ b/cmd/gobroke/main.go @@ -0,0 +1 @@ +package gobroke diff --git a/conf/_testdata/test.json b/conf/_testdata/test.json new file mode 100644 index 0000000..c0c06ba --- /dev/null +++ b/conf/_testdata/test.json @@ -0,0 +1,82 @@ +{ + "default_username": "default_user", + "1tun": true, + "cache_db": "/var/cache/gobroke.db", + "cache_perms": { + "file": { + "user": "", + "group": "", + "mode": 384 + }, + "dir": { + "user": "", + "group": "", + "mode": 448 + } + }, + "tunnels": [ + { + "tun_id": 123, + "addr": "203.0.113.1", + "mtu": 1450, + "username": "specific_user", + "update_key": "abcdef", + "cfg_tpls": [ + { + "tpl": "/etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl", + "dest": "/etc/dnsmasq.d/ra_dhcpv6.conf", + "perms": { + "file": { + "user": "", + "group": "", + "mode": 384 + }, + "dir": { + "user": "", + "group": "", + "mode": 448 + } + }, + "cmds": [ + { + "bin": "/usr/local/bin/somecmd", + "args": [ + "-f", "foo" + ], + "isol8_env": false, + "env": [ + "SOMEENV=SOMEVAL" + ], + "on_change": true, + "is_tpl": false + } + ] + }, + { + "tpl": "/etc/gobroke/tpl/stat.tpl", + "dest": "/tmp/gobroke.dump" + } + ], + "cmds": [ + { + "bin": "systemctl", + "args": [ + "restart", + "someservice" + ], + "on_change": true + } + ] + }, + { + "tun_id": 456, + "username": "specific_user", + "update_key": "defghi" + } + ], + "cmds": [ + { + "bin": "/usr/local/bin/alltunsprogram" + } + ] +} diff --git a/conf/_testdata/test.toml b/conf/_testdata/test.toml new file mode 100644 index 0000000..ca9b987 --- /dev/null +++ b/conf/_testdata/test.toml @@ -0,0 +1,289 @@ +# DefaultUsername specifies the default username to use for +# authenticating to tunnelbroker.net. +# It is optional, as the username can be specified for each Tunnel, +# but at least one or the other *must* be provided. +# This makes it easier if you have multiple tunnels under the same account +# (as possible in higher levels of HE IPv6 certification). +# If a username is specified in Tunnel.Username, it will be used. +# If not (and, of course, DefaultUsername is specified), then +# DefaultUsername will be used for that Tunnel. +DefaultUsername = "default_user" + +# If SingleTunnel is true, each Tunnel below will be run in order instead of +# concurrently. +# If there is any concern about race conditions (e.g. the same service being +# restarted by multiple tunnels, etc.), then it is HIGHLY RECOMMENDED +# you set this to true. +SingleTunnel = true + +# CacheDbPath is entirely optional. +# If not provided, results will be cached in RAM (and thus lost on reboot +# or program termination/restart). +# (This can be explicitly specified by using the value ':memory:'.) +# If provided, it should be a path to a file to use as a SQLite3 database +# that holds cached information. +# The information that is cached contains only: +# * each Tunnel.TunnelID +# * the associated tunnelbroker.FetchedTunnel +# * a CRC32 of all configuration (as defined in this file) for that Tunnel +# The UpdateKey and other configuration defined here (aside from +# Tunnel.TunnelID, and Tunnel.ExplicitClientIP if specified) are +# NOT stored. +# Any tunnel present in a persistent cache DB but *not* defined in the +# running GoBroke config will be removed. +# Note that the cache DB primary key is based on the Tunnel.TunnelID, +# as one cannot define multiple client endpoints for the same tunnel. +CacheDbPath = '/var/cache/gobroke.db' + +# CacheDbPerms specify the permissions for CacheDbPath. +# This directive is completely optional, and is +# ignored if CacheDbPath is ":memory:" (or unspecified). +# If not specified (and CacheDbPath is persistent), +# then the runtime user's umask and effective UID/GID +# is used if creating a new database file. +# If the file exists and permissions are defined, they will +# be enforced. +# If the file exists but no permissions are defined, they +# will be left as-is. +[CacheDbPerms] + # Permissions are/may be defined for both the file being written + # and the parent directory (see below). + [CacheDbPerms.File] + # The User is optional. + # If unspecified, the default behavir mentioned above is performed. + # If specified as an empty string, the runtime EUID is enforced. + # Otherwise it may be a username or a UID (checked in that order). + User = "" + # Group is also optional, and follows the same logic except + # for EGID/groupnames/GIDs. + Group = "" + # Mode is optional also. + # It *must* be equal to the octal mode bits (e.g. it must be an + # unsigned integer), but may be represented in multiple ways. + # e.g.: + # Mode = 0o0600 + # Mode = 0o600 + # Mode = 0x0180 + # Mode = 0x180 + # Mode = 0b110000000 + # Mode = 384 + # All evaluate to the exact same value in TOML: + # https://toml.io/en/v1.0.0#integer + # For consistency with `chmod(1)`, it is recommended to use the + # octal representation (0o0600 or 0o600 above). + # If you need help determining what number you should actually use, + # you can use the calculator here: + # https://rubendougall.co.uk/projects/permissions-calculator/ + # (source: https://github.com/Ruben9922/permissions-calculator ) + # (Supports "special" bits) + # or here: + # https://wintelguy.com/permissions-calc.pl + # (beware of ads) + # (provides an explanation of the bits) + # Or see https://en.wikipedia.org/wiki/Chmod + Mode = 0o0600 + # Dir permissions specifiy permissions/ownership of the parent directory of the cache DB. + # The same rules, logic, behavior, etc. as in CacheDbPerms.File apply here. + [CacheDbPerms.Dir] + User = "" + Group = "" + Mode = 0o0700 + + +############# +## Tunnels ## +############# + +# Each Tunnel represents a single tunnelbroker.net tunnel configuration. +# Note that each Tunnel is run concurrently. If this is undesired due to +# potential race conditions, set the root-level directive SingleTunnel +# to true. +[[Tunnel]] + # The TunnelID can be found by logging into https://tunnelbroker.net/ and, + # at the "Main Page" that loads when logging in, clicking on the desired + # tunnel name. + # The tunnel ID is then displayed in both the URL bar: + # https://tunnelbroker.net/tunnel_detail.php?tid= + # And as the first line on the first tab ("IPv6 Tunnel" tab), + # labeled "Tunnel ID". + TunnelID = 123 + # If you wish to use a different or explicit "Client IPv4 address", + # this can be specified via ExplicitClientIP. + # If it is empty or is not specified, the public IP of this host will be determined + # via an external service. + # This *must* be an IPv4 address (if specified). + ExplicitClientIP = '203.0.113.1' + # If you have specified a custom MTU under the "Advanced" tab for this tunnel, + # you can set this value here. + # If you have not set a custom one, leave this option unspecified; + # the default (and maximum allowed), 1480 MTU, will be used in that case. + MTU = 1450 + # The Username field is optional IF DefaultUsername was specified. + # This also allows you to specify tunnels from different accounts + # by providing a tunnel-specific username. + Username = "specific_user" + # The UpdateKey can be found under the "Advanced" tab on your tunnelbroker.net + # tunnel's page, labeled "Update Key". + # Your real token is likely to be a bit longer and more random. + # This token is used to not only update the client-side tunnel IP but also to + # query the HE Tunnelbroker "API" (it's really just a single endpoint) + # to get the tunnel configuration. + UpdateKey = "abcdef" + + + ###################### + ## Config Templates ## + ###################### + + # Each ConfigTemplate consists of a path to a template file and a destination + # file at the bere minimum. In addition, Commands may be provided. + # Any paths leading up to Destination that don't exist will (attempt to be) + # created. + # The template is always rendered in memory, but the destination is only written + # if: + # * The Destination doesn't exist + # * The Destination differs from the buffered rendering of the template + # Commands are optional, and are a list of commands to be run. + # Their running may be restricted to only if the tunnel information/IP + # information has changed, always run, or the inverse of all conditions. + [[Tunnel.ConfigTemplate]] + # Template points to where the template file can be found. + # It must be in a Golang text/template syntax/format; see: + # https://pkg.go.dev/text/template + # Refer to the library's definition of the tunnelbroker.FetchedTunnel struct; + # this is the object that is passed to the template. + Template = "/etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl" + # Destination is the file to write to. + # It will only be written to if: + # * The path does not exist + # * The path exists but is different from the in-memory rendered buffer + # An attempt will be made to create any leading components that are not + # present. + # It is recommended to enforce permissions/ownership of these via the + # Commands. + Destination = "/etc/dnsmasq.d/ra_dhcpv6.conf" + + + ################################# + ## Config Template Permissions ## + ################################# + + # Permissions can be defined for the Destionation file. + # They are completely optional, in which case the default umask, user, + # group, etc. for the runtime user will be used, and permissions/ownership + # will not be enforced for existing Destination files. + # It follows the same syntax, logic, behavior, etc. as CacheDbPerms. + [[Tunnel.ConfigTemplate.Permissions]] + [[Tunnel.ConfigTemplate.Permissions.File]] + User = "" + Group = "" + Mode = 0o0600 + [[Tunnel.ConfigTemplate.Permissions.Dir]] + User = "" + Group = "" + Mode = 0o0700 + + + ############################## + ## Config Template Commands ## + ############################## + + # Commands are a collection of commands to run as part of this template + # run. + # Multiple Commands may be specified; they will be run in the order specified. + # The below Command would be equivalent to: + # SOMEENV=SOMEVAL /usr/local/bin/somecmd -f foo + # on the shell. + [[Tunnel.ConfigTemplate.Command]] + # ProgramPath should be the absolute path to the binary to run. + # It behaves as an (os/)exec.Cmd.Path (https://pkg.go.dev/os/exec#Cmd), + # It is recommended to use an absolute path. + ProgramPath = '/usr/local/bin/somecmd' + # Args are optional for a Command. + # They should conform to the rules for (os/)exec.Cmd.Args. + Args = [ + '-f', 'foo', + ] + # If IsolatedEnv is false (the default), the runtime environment variables + # will be applied to the command. + # If true, *only* the EnvVars, if specified, will be used for the spawned + # command (an empty environment will be used if IsolateEnv is true and + # no EnvVars are specified). + IsolatedEnv = false + # If provided, EnvVars can be used to add/replace environment variables. + # They should conform to the rules for (os/)exec.Cmd.Env. + # Whether they are added to/selectively replace or completely replace + # the current runtime environment variables depends on how IsolateEnv + # is configured. + EnvVars = [ + 'SOMEENV=SOMEVAL', + ] + # If OnChange is true, this Command will run *only if SOMETHING CHANGED*. + # (e.g. a /48 was added to the tunnel, the client IP is different, etc.) + # If false, this Command will run *only if NOTHING CHANGED*. + # If unspecified, the default is to always run this command regardless + # of change status. + # The very first (successful) run of a Tunnel is considered a "change", + # as is writing out this template to disk as a new file. + OnChange = true + # By default, this Command will be run literally/as-is. + # However, in some cases it may be useful to dynamically template out + # commands to run. + # If IsTemplate is set to true, then this Command.ProgramPath, each + # of the Command.Args, and each of the Command.EnvVars will be + # treated as Golang text/template strings as well, and will also + # be passed a tunnelbroker.FetchedTunnel. + # Note that if IsolateEnv is false, runtime/inherited environment + # variables will *not* be templated. + # It is recommended to not enable this unless necessary as it can add + # a non-negligible amount of resource overhead/execution time. + IsTemplate = false + + ####################################################################### + + # Multiple ConfigTemplates may be specified. + [[Tunnel.ConfigTemplate]] + Template = "/etc/gobroke/tpl/stat.tpl" + Destination = "/tmp/gobroke.dump" + + + ##################### + ## Tunnel Commands ## + ##################### + + # Each Tunnel also supports its *own* commands. The syntax, spcification, + # behavior, etc. is the same as the Tunnel.ConfigTemplate.Command. + # These are executed after all Tunnel.ConfigTemplate (if any) are executed. + # This is particularly useful for consolidating service restarts. + [[Tunnel.Command]] + ProgramPath = 'systemctl' + Args = [ + 'restart', + 'someservice', + ] + # OnChange in a Tunnel.Command is scoped to any updates of the tunnel + # and any changes in ANY of the Tunnel.ConfigTemplate specified + # for this Tunnel (if true and ConfigTemplate were specified). + OnChange = true + +############################################################################### + +# Multiple tunnel configurations are supported as well. +[[Tunnel]] + TunnelID = 456 + Username = "specific_user" + UpdateKey = "defghi" + + +###################### +## General Commands ## +###################### + +# Command items may be specified at the root level as well. +# The syntax is like all other Commands items, with two exceptions: +# * There is no templating performed... +# * As such, there is no IsTemplate directive for these. +# A root-level Command is run after all tunnels complete. +# The OnChange directive is true if any Tunnels result in any changes. +[[Command]] + ProgramPath = "/usr/local/bin/alltunpsrogram" diff --git a/conf/_testdata/test.xml b/conf/_testdata/test.xml new file mode 100644 index 0000000..b061b33 --- /dev/null +++ b/conf/_testdata/test.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + -f + foo + + + SOMEENV=SOMEVAL + + + + + + + + + + restart + someservice + + + + + + + + + + diff --git a/conf/_testdata/test.yml b/conf/_testdata/test.yml new file mode 100644 index 0000000..aaa5914 --- /dev/null +++ b/conf/_testdata/test.yml @@ -0,0 +1,59 @@ +# See the example TOML for detailed comments and explanations. +Default Username: default_user + +NoGoTunnel: true + +Cache Database Path: /var/cache/gobroke.db + +Cache Database Permissions: + File: + User: '' + Group: '' + Mode: 384 + Directory: + User: '' + Group: '' + Mode: 448 + +Tunnels: + - Tunnel ID: 123 + Explicit Client IP Address: 203.0.113.1 + MTU: 1450 + Username: specific_user + Update Key: abcdef + Configuration File Templates: + - Template File Path: /etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl + Destination File Path: /etc/dnsmasq.d/ra_dhcpv6.conf + Permissions and Ownership: + File: + User: '' + Group: '' + Mode: 384 + Directory: + User: '' + Group: '' + Mode: 448 + Commands: + - Program Path: /usr/local/bin/somecmd + Arguments: + - '-f' + - 'foo' + Isolated Environment: false + Environment Variables: + - SOMEENV=SOMEVAL + On Change: true + Is Template: false + - Template File Path: /etc/gobroke/tpl/stat.tpl + Destination File Path: /tmp/gobroke.dump + Commands: + - Program Path: systemctl + Arguments: + - restart + - someservice + On Change: true + - Tunnel ID: 456 + Username: specific_user + Update Key: defghi + +Commands: + - Program Path: /usr/local/bin/alltunsprogram diff --git a/conf/consts.go b/conf/consts.go new file mode 100644 index 0000000..e564562 --- /dev/null +++ b/conf/consts.go @@ -0,0 +1,9 @@ +package conf + +import ( + "github.com/go-playground/validator/v10" +) + +var ( + validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled()) +) diff --git a/conf/errs.go b/conf/errs.go new file mode 100644 index 0000000..e410ef5 --- /dev/null +++ b/conf/errs.go @@ -0,0 +1,10 @@ +package conf + +import ( + `errors` +) + +var ( + ErrMissingUser error = errors.New("at least one tunnel is missing a username and no default username was provided") + ErrUnkownSyntax error = errors.New("unknown/unsupported configuration syntax") +) diff --git a/conf/funcs.go b/conf/funcs.go new file mode 100644 index 0000000..da2b453 --- /dev/null +++ b/conf/funcs.go @@ -0,0 +1,75 @@ +package conf + +import ( + `encoding/json` + + "github.com/BurntSushi/toml" + "github.com/creasty/defaults" + "github.com/goccy/go-yaml" + "r00t2.io/sysutils/paths" +) + +func NewConfig(path string) (cfg *Config, err error) { + + var b []byte + + if err = paths.RealPath(&path); err != nil { + return + } + + cfg, err = NewConfigFromBytes(b) + + return +} + +func NewConfigFromBytes(b []byte) (cfg *Config, err error) { + + if err = json.Unmarshal(b, &cfg); err != nil { + if err = yaml.Unmarshal(b, &cfg); err != nil { + if err = toml.Unmarshal(b, &cfg); err != nil { + if err = toml.Unmarshal(b, &cfg); err != nil { + err = ErrUnkownSyntax + return + } + } + } + } + + if err = defaults.Set(cfg); err != nil { + return + } + + if cfg.CacheDB != ":memory:" { + if err = paths.RealPath(&cfg.CacheDB); err != nil { + return + } + } + + if err = validate.Struct(cfg); err != nil { + return + } + + for _, t := range cfg.Tunnels { + t.cfg = cfg + if t.Username == nil { + if cfg.Username == nil { + err = ErrMissingUser + return + } else { + t.Username = cfg.Username + } + } + if t.TemplateConfigs != nil && len(t.TemplateConfigs) > 0 { + for _, tpl := range t.TemplateConfigs { + if err = paths.RealPath(&tpl.Template); err != nil { + return + } + if err = paths.RealPath(&tpl.Dest); err != nil { + return + } + } + } + } + + return +} diff --git a/conf/funcs_test.go b/conf/funcs_test.go new file mode 100644 index 0000000..d26398a --- /dev/null +++ b/conf/funcs_test.go @@ -0,0 +1,50 @@ +package conf + +import ( + `embed` + `encoding/xml` + `fmt` + "testing" + + `github.com/BurntSushi/toml` + `github.com/goccy/go-yaml` +) + +var ( + //go:embed "_testdata" + testData embed.FS +) + +func TestConf(t *testing.T) { + var err error + var cfg *Config + var b []byte + + if b, err = testData.ReadFile("_testdata/test.json"); err != nil { + t.Fatal(err) + } + if cfg, err = NewConfigFromBytes(b); err != nil { + t.Fatal(err) + } + + /* + spew.Dump(cfg) + return + */ + + if b, err = toml.Marshal(cfg); err != nil { + t.Fatal(err) + } + fmt.Println(string(b)) + + if b, err = xml.MarshalIndent(cfg, "", " "); err != nil { + t.Fatal(err) + } + fmt.Println(string(b)) + + if b, err = yaml.Marshal(cfg); err != nil { + t.Fatal(err) + } + fmt.Println(string(b)) + +} diff --git a/conf/funcs_tunnel.go b/conf/funcs_tunnel.go new file mode 100644 index 0000000..8f52a95 --- /dev/null +++ b/conf/funcs_tunnel.go @@ -0,0 +1 @@ +package conf diff --git a/conf/types.go b/conf/types.go new file mode 100644 index 0000000..5fbf881 --- /dev/null +++ b/conf/types.go @@ -0,0 +1,129 @@ +package conf + +import ( + "encoding/xml" + `net` + `os` + + `r00t2.io/gobroke/tplCmd` +) + +// Config represents a configuration file. +type Config struct { + XMLName xml.Name `json:"-" toml:"-" xml:"config" yaml:"-" ` + /* + Username, if provided, will be the default username used for any Tunnel that does not specify one (via Tunnel.Username). + This should be the username used when authenticating to tunnelbroker.net. + It is optional, as the username can be specified/overridden for each Tunnel, but at least one or the other *must* be provided. + This makes it easier if you have multiple tunnels under the same account. + If a username is specified in Tunnel.Username, it will be used. + If not (and, of course, Config.Username is specified), then Config.Username will be used for that Tunnel. + */ + Username *string `json:"default_username,omitempty" toml:"DefaultUsername,omitempty" xml:"defaultUser,attr,omitempty" yaml:"Default Username,omitempty"` + // SingleTunnel, if true, will suppress goroutine-management of tunnels and instead execute them sequentially instead. + SingleTunnel bool `json:"1tun,omitempty" toml:"SingleTunnel,omitempty" xml:"oneTun,attr,omitempty" yaml:"NoGoTunnel,omitempty"` + // CacheDB, if specified, is a path to a SQLite3 DB on-disk to make cached information persistent across reboots. + CacheDB string `json:"cache_db,omitempty" toml:"CacheDbPath,omitempty" xml:"cacheDb,attr,omitempty" yaml:"Cache Database Path,omitempty" default:":memory:" validate:"omitempty,filepath|eq=:memory:"` + // CacheDbPerms specifies the optional permissions for the file and parent directory for CacheDB; only used if persistent cache. + CacheDbPerms *Perms `json:"cache_perms,omitempty" toml:"CacheDbPerms,omitempty" xml:"cachePerms,omitempty" yaml:"Cache Database Permissions,omitempty"` + // Tunnels contains one or more tunnel configurations. + Tunnels []*Tunnel `json:"tunnels" toml:"Tunnel" xml:"tunnels>tunnel" yaml:"Tunnels" validate:"required"` + /* + Cmds are executed, in order, *after* all Tunnel configurations have been run. + Unlike in Tunnel and ConfigTemplate, no templating on these commands is performed. + */ + Cmds []tplCmd.Cmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty"` +} + +// Tunnel represents a single tunnel configuration from tunnelbroker.net. +type Tunnel struct { + XMLName xml.Name `json:"-" toml:"-" xml:"tunnel" yaml:"-"` + /* + TunnelID can be found by logging into https://tunnelbroker.net/ and, at the "Main Page" that loads + when logging in, clicking on the desired tunnel name. The tunnel ID is then displayed in both the URL bar: + https://tunnelbroker.net/tunnel_detail.php?tid= + And as the first line on the first tab ("IPv6 Tunnel" tab), labeled "Tunnel ID". + */ + TunnelID uint `json:"tun_id" toml:"TunnelID" xml:"id,attr" yaml:"Tunnel ID" validate:"required,ge=1"` + /* + ExplicitAddr, if provided, will be used as the tunnelbroker.FetchedTunnel.CurrentIPv4. + If not provided, this will be fetched dynamically from an external source. + */ + ExplicitAddr *net.IP `json:"addr,omitempty" toml:"ExplicitClientIP,omitempty" xml:"addr,attr,omitempty" yaml:"Explicit Client IP Address,omitempty"` + /* + MTU should be specified if you have defined a custom one (under the "Advanced" tab for this tunnel at tunnlebroker.net). + If you did not change this, the default is 1480 (the maximum allowed), and the default value of this struct field + on configuration parsing will reflect this. + */ + MTU uint `json:"mtu,omitempty" toml:"MTU,omitempty" xml:"mtu,attr,omitempty" yaml:"MTU,omitempty" default:"1480" validate:"required,gt=0,le=1480"` + /* + Username field is optional IF DefaultUsername was specified. + This also allows you to specify tunnels from different accounts + by providing a tunnel-specific username. + */ + Username *string `json:"username,omitempty" toml:"Username,omitempty" xml:"username,attr,omitempty" yaml:"Username,omitempty"` + /* + UpdateKey can be found under the "Advanced" tab on your tunnelbroker.net tunnel's page, labeled "Update Key". + This token is used to not only update the client-side tunnel IP but also to query the HE Tunnelbroker "API" + (it's really just a single endpoint) to get the tunnel information necessary for local configuration. + */ + UpdateKey string `json:"update_key" toml:"UpdateKey" xml:"key,attr" yaml:"Update Key" validate:"required"` + // TemplateConfgs is optional. It holds templates that will be executed in order given. See ConfigTemplate. + TemplateConfigs []ConfigTemplate `json:"cfg_tpls" toml:"ConfigTemplate" xml:"config>tpl" yaml:"Configuration File Templates"` + /* + Cmds are executed, in order, *after* all tunnel updates/fetching and the templating has completed (if any specified). + Each command will also have tunnelbroker.FetchedTunnel templated to it like TemplateConfigs/ConfigTemplate.Commands, + so they may be templated as necessary. + */ + Cmds []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty"` + // cfg is the parent Config. + cfg *Config +} + +/* + ConfigTemplate allows the templating of configuration files, etc. from the tunnel information. + Templates are executed *after* the IP update (if an update was necessary), but are always *run*. + ConfigTemplate.Dest will only be written to if: + + * The file does not exist (yet), or + * The templated content differs from the file on disk +*/ +type ConfigTemplate struct { + XMLName xml.Name `json:"-" toml:"-" xml:"tpl" yaml:"-"` + /* + Template is the path to the template file on disk. + It must follow the syntax, rules, etc. of a Golang (text/)template.Template (https://pkg.go.dev/text/template#Template). + The struct passed to it is a tunnelbroker.FetchedTunnel. + */ + Template string `json:"tpl" toml:"Template" xml:"tpl,attr" yaml:"Template File Path" validate:"required,filepath"` + // Dest contains the filepath that the Template should be written out to. + Dest string `json:"dest" toml:"Destination" xml:"dest,attr" yaml:"Destination File Path" validate:"required,filepath"` + // Perms allows specifying permissions/ownerships, if the curent user has the capability to do so. + Perms *Perms `json:"perms,omitempty" toml:"Permissions,omitempty" xml:"perms,omitempty" yaml:"Permissions and Ownership,omitempty"` + // Commands specifiies commands to run after this ConfigTemplate run. + Commands []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"cmds>cmd,omitempty" yaml:"Commands,omitempty"` +} + +type Perms struct { + // File specifies the desired permissions/ownership of the target file. + File *PermSpec `json:"file,omitempty" toml:"File,omitempty" xml:"file,omitempty" yaml:"File,omitempty"` + // ParentDir specifies the desired permissions/ownership of the parent ("dirname") of File. + ParentDir *PermSpec `json:"dir,omitempty" toml:"Dir,omitempty" xml:"dir,omitempty" yaml:"Directory,omitempty"` +} + +type PermSpec struct { + /* + User is the username or UID (tried in that order) to chown. + If specified as an empty string, the current/runtime UID will be used. + If unspecified, UID will not be enforced. + */ + User *string `json:"user,omitempty" toml:"User,omitempty" xml:"user,attr,omitempty" yaml:"User,omitempty"` + /* + Group is the groupname or GID (tried in that order) to chown. + If specified as an empty string, the current/runtime GID will be used. + If unspecified, GID will not be enforced. + */ + Group *string `json:"group,omitempty" toml:"Group,omitempty" xml:"group,attr,omitempty" yaml:"Group,omitempty"` + // Mode is the permission mode bitset. If unspecified, mode will not be enforced. + Mode *os.FileMode `json:"mode,omitempty" toml:"Mode,omitempty" xml:"mode,attr,omitempty" yaml:"Mode,omitempty"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a9bdf9 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module r00t2.io/gobroke + +go 1.23.3 + +require ( + github.com/BurntSushi/toml v1.4.0 + github.com/creasty/defaults v1.8.0 + github.com/go-playground/validator/v10 v10.23.0 + github.com/go-resty/resty/v2 v2.16.2 + github.com/goccy/go-yaml v1.15.7 + github.com/jmoiron/sqlx v1.4.0 + r00t2.io/sysutils v1.12.0 +) + +require ( + github.com/djherbis/times v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.16.0 // indirect + r00t2.io/goutils v1.7.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c0710b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,61 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= +github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= +github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98= +github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc= +r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= +r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= +r00t2.io/sysutils v1.12.0 h1:Ce3qUOyLixE1ZtFT/+SVwOT5kSkzg5+l1VloGeGugrU= +r00t2.io/sysutils v1.12.0/go.mod h1:bNTKNBk9MnUhj9coG9JBNicSi5FrtJHEM645um85pyw= diff --git a/tplCmd/funcs_cmd.go b/tplCmd/funcs_cmd.go new file mode 100644 index 0000000..8b77879 --- /dev/null +++ b/tplCmd/funcs_cmd.go @@ -0,0 +1,34 @@ +package tplCmd + +import ( + `os` + `os/exec` +) + +/* + ToCmd returns an (os/)exec.Cmd from a Cmd. + + err will always be nil for now, but is still returned and should be handled + for future-proofing. +*/ +func (c *Cmd) ToCmd() (cmd *exec.Cmd, err error) { + + var envs []string + + if c.Args != nil && len(c.Args) > 0 { + cmd = exec.Command(c.Program, c.Args...) + } else { + cmd = exec.Command(c.Program) + } + + if !c.IsolateEnv { + envs = os.Environ() + } + if c.EnvVars != nil && len(c.EnvVars) > 0 { + envs = append(envs, c.EnvVars...) + } + + cmd.Env = envs + + return +} diff --git a/tplCmd/funcs_templatecmd.go b/tplCmd/funcs_templatecmd.go new file mode 100644 index 0000000..6a329b0 --- /dev/null +++ b/tplCmd/funcs_templatecmd.go @@ -0,0 +1,73 @@ +package tplCmd + +import ( + `bytes` + `os` + `os/exec` + `text/template` +) + +// ToCmd returns an (os/)exec.Cmd from a TemplateCmd. t should be a tunnelbroker.FetchedTunnel, generally. +func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) { + + var progName string + var envs []string + var tpl *template.Template + var args []string + var buf *bytes.Buffer = new(bytes.Buffer) + + buf.Reset() + if tpl, err = template.New("").Parse(c.Program); err != nil { + return + } + if err = tpl.Execute(buf, t); err != nil { + return + } + progName = buf.String() + + if c.Args != nil && len(c.Args) > 0 { + args = make([]string, len(c.Args)) + for idx, arg := range c.Args { + buf.Reset() + if tpl, err = template.New("").Parse(arg); err != nil { + return + } + if err = tpl.Execute(buf, t); err != nil { + return + } + args[idx] = buf.String() + } + + } + + if c.EnvVars != nil && len(c.EnvVars) > 0 { + envs = make([]string, len(c.EnvVars)) + for idx, env := range c.EnvVars { + buf.Reset() + if tpl, err = template.New("").Parse(env); err != nil { + return + } + if err = tpl.Execute(buf, t); err != nil { + return + } + envs[idx] = buf.String() + } + } + if !c.IsolateEnv { + if envs != nil && len(envs) > 0 { + envs = append(os.Environ(), envs...) + } else { + envs = os.Environ() + } + } + + if args != nil && len(args) > 0 { + cmd = exec.Command(progName, args...) + } else { + cmd = exec.Command(progName) + } + + cmd.Env = envs + + return +} diff --git a/tplCmd/types.go b/tplCmd/types.go new file mode 100644 index 0000000..2388203 --- /dev/null +++ b/tplCmd/types.go @@ -0,0 +1,21 @@ +package tplCmd + +import ( + `encoding/xml` +) + +// Cmd is a command that is executed without any templating. +type Cmd struct { + XMLName xml.Name `json:"-" toml:"-" xml:"cmd" yaml:"-"` + Program string `json:"bin" toml:"ProgramPath" xml:"bin,attr" yaml:"Program Path" validate:"required,file"` + Args []string `json:"args" toml:"Argument" xml:"args>arg" yaml:"Arguments"` + IsolateEnv bool `json:"isol8_env" toml:"IsolatedEnv" xml:"isol8Env,attr" yaml:"Isolated Environment"` + EnvVars []string `json:"env" toml:"EnvVars" xml:"envs>env" yaml:"Environment Variables"` + OnChanges *bool `json:"on_change,omitempty" toml:"OnChange,omitempty" xml:"onChange,attr,omitempty" yaml:"On Change,omitempty"` +} + +// TemplateCmd is a command that supports templating. (It's in its own package to avoid a cyclic dependency.) +type TemplateCmd struct { + *Cmd `yaml:",inline"` + IsTemplate bool `json:"is_tpl" toml:"IsTemplate" xml:"isTpl,attr" yaml:"Is Template"` +} diff --git a/tunnelbroker/consts.go b/tunnelbroker/consts.go new file mode 100644 index 0000000..948530d --- /dev/null +++ b/tunnelbroker/consts.go @@ -0,0 +1,23 @@ +package tunnelbroker + +const ( + wanIpUrl string = "https://c4.r00t2.io/ip" + // https://forums.he.net/index.php?topic=3153.0 + // If no TID is provided, all tunnels are returned. + /* + All-tunnels mode can *only* use the account's password, + the TID-specified can *only* use the Update Key. + */ + infoBaseUrl string = "https://tunnelbroker.net/tunnelInfo.php" + updateBaseUrl string = "https://ipv4.tunnelbroker.net/nic/update" + infoTidParam string = "tid" + updateTidParam string = "hostname" + /* + NOTE: + This parameter is only required if the client's WAN IP + does not match the desired tunnel address. + If left off, it defaults to client IP (as seen by the webserver). + */ + updateIpParam string = "myip" + noTunBody string = "No tunnels found" +) diff --git a/tunnelbroker/funcs.go b/tunnelbroker/funcs.go new file mode 100644 index 0000000..a1678a5 --- /dev/null +++ b/tunnelbroker/funcs.go @@ -0,0 +1,9 @@ +package tunnelbroker + +// NewClient reuturns a Client. +func NewClient() (c *Client, err error) { + + // TODO + + return +} diff --git a/tunnelbroker/types.go b/tunnelbroker/types.go new file mode 100644 index 0000000..ef4e542 --- /dev/null +++ b/tunnelbroker/types.go @@ -0,0 +1,49 @@ +package tunnelbroker + +import ( + `encoding/xml` + `net` + `net/netip` + + `github.com/go-resty/resty/v2` + `r00t2.io/gobroke/conf` +) + +type Client struct { + tunCfg *conf.Config + myAddr net.IP +} + +type TunnelList struct { + XMLName xml.Name `json:"-" xml:"tunnels" yaml:"-"` + Tunnels []*Tunnel `json:"tunnels" xml:"tunnel" yaml:"Tunnels"` +} + +type Tunnel struct { + XMLName xml.Name `json:"-" xml:"tunnel" yaml:"-"` + ID uint `json:"id" xml:"id,attr" yaml:"ID" db:"tun_id"` + Description string `json:"desc" xml:"description" yaml:"Description" db:"desc"` + ServerIPv4 net.IP `json:"tgt_v4" xml:"serverv4" yaml:"IPv4 Tunnel Target" db:"server_v4"` + ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"` + ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"` + ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"` + Routed64 netip.Prefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"` + Routed48 *netip.Prefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"` + RDNS1 *string `json:"rdns_1,omitempty" xml:"rdns1,omitempty" yaml:"RDNS #1,omitempty" db:"rdns_1"` + RDNS2 *string `json:"rdns_2,omitempty" xml:"rdns2,omitempty" yaml:"RDNS #2,omitempty" db:"rdns_2"` + RDNS3 *string `json:"rdns_3,omitempty" xml:"rdns3,omitempty" yaml:"RDNS #3,omitempty" db:"rdns_3"` + RDNS4 *string `json:"rdns_4,omitempty" xml:"rdns4,omitempty" yaml:"RDNS #4,omitempty" db:"rdns_4"` + RDNS5 *string `json:"rdns_5,omitempty" xml:"rdns5,omitempty" yaml:"RDNS #5,omitempty" db:"rdns_5"` + client *Client + heClient *resty.Client + tunCfg *conf.Tunnel +} + +type FetchedIP struct { + NewClientIPv4 net.IP `json:"new_client_v4" xml:"newClientv4,attr" yaml:"Evaluated IPv4 Client Address" db:"client_ip"` +} + +type FetchedTunnel struct { + *Tunnel + *FetchedIP +} diff --git a/version/consts.go b/version/consts.go new file mode 100644 index 0000000..04ec47a --- /dev/null +++ b/version/consts.go @@ -0,0 +1,32 @@ +package version + +import ( + "regexp" +) + +/* +These variables are automatically handled by the build script. + +DO NOT MODIFY THESE VARIABLES. +Refer to /build.sh for how these are generated at build time and populated. +*/ +var ( + sourceControl string = "git" + version string = "(unknown)" + commitHash string + commitShort string + numCommitsAfterTag string + isDirty string + buildTime string + buildUser string + buildSudoUser string + buildHost string +) + +var ( + patchRe *regexp.Regexp = regexp.MustCompile(`^(?P[0-9+])(?P
-[0-9A-Za-z.-]+)?(?P\+[0-9A-Za-z.-]+)?$`)
+	patchReIsolated *regexp.Regexp = regexp.MustCompile(`^([0-9]+)(?:[-+](.*)?)?$`)
+)
+
+// Ver is populated by main() from the build script and used in other places.
+var Ver *BuildInfo
diff --git a/version/funcs.go b/version/funcs.go
new file mode 100644
index 0000000..6624bd3
--- /dev/null
+++ b/version/funcs.go
@@ -0,0 +1,144 @@
+package version
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"golang.org/x/mod/semver"
+)
+
+// Version returns the build information. See build.sh.
+func Version() (b *BuildInfo, err error) {
+
+	var n int
+	var s string
+	var sb strings.Builder
+	var ok bool
+	var canonical string
+	var build strings.Builder
+	// Why a map?
+	// I forget but I had a reason for it once upon a time.
+	var raw map[string]string = map[string]string{
+		"sourceControl":  sourceControl,
+		"tag":            version,
+		"hash":           commitHash,
+		"shortHash":      commitShort,
+		"postTagCommits": numCommitsAfterTag,
+		"dirty":          isDirty,
+		"time":           buildTime,
+		"user":           buildUser,
+		"sudoUser":       buildSudoUser,
+		"host":           buildHost,
+	}
+	var i BuildInfo = BuildInfo{
+		SourceControl: raw["sourceControl"],
+		TagVersion:    raw["tag"],
+		// PostTagCommits: 0,
+		CommitHash:    raw["hash"],
+		CommitId:      raw["shortHash"],
+		BuildUser:     raw["user"],
+		RealBuildUser: raw["sudoUser"],
+		// BuildTime:     time.Time{},
+		BuildHost: raw["host"],
+		Dirty:     false,
+		isDefined: false,
+		raw:       raw,
+	}
+
+	if s, ok = raw["postTagCommits"]; ok && strings.TrimSpace(s) != "" {
+		if n, err = strconv.Atoi(s); err == nil {
+			i.PostTagCommits = uint(n)
+		}
+	}
+
+	if s, ok = raw["time"]; ok && strings.TrimSpace(s) != "" {
+		if n, err = strconv.Atoi(s); err == nil {
+			i.BuildTime = time.Unix(int64(n), 0).UTC()
+		}
+	}
+
+	switch strings.ToLower(raw["dirty"]) {
+	case "1":
+		i.Dirty = true
+	case "0", "":
+		i.Dirty = false
+	}
+
+	// Build the short form. We use this for both BuildInfo.short and BuildInfo.verSplit.
+	if i.TagVersion == "" {
+		sb.WriteString(i.SourceControl)
+	} else {
+		sb.WriteString(i.TagVersion)
+	}
+	/*
+		Now the mess. In order to conform to SemVer 2.0 (the spec this code targets):
+
+		1.) MAJOR.
+		2.) MINOR.
+		3.) PATCH
+		4.) -PRERELEASE (OPTIONAL)
+			(git commit, if building against a commit made past 1-3. Always included if untagged.)
+		5.) +BUILDINFO (OPTIONAL)
+			("+x[.y]", where x is # of commits past 4, or tag commit if 4 is empty. 0 is valid.
+			 y is optional, and is the string "dirty" if it is a "dirty" build - that is, uncommitted/unstaged changes.
+			 if x and y would be 0 and empty, respectively, then 5 is not included.)
+
+		1-3 are already written, or the source control software used if not.
+
+		Technically 4 and 5 are only included if 3 is present. We force patch to 0 if it's a tagged release and patch isn't present --
+		so this is not relevant.
+	*/
+	// PRERELEASE
+	if i.TagVersion == "" || i.PostTagCommits > 0 {
+		// We use the full commit hash for git versions, short identifier for tagged releases.
+		if i.TagVersion == "" {
+			i.Pre = i.CommitHash
+		} else {
+			i.Pre = i.CommitId
+		}
+		sb.WriteString(fmt.Sprintf("-%v", i.Pre))
+	}
+	// BUILD
+	if i.PostTagCommits > 0 || i.Dirty {
+		build.WriteString(strconv.Itoa(int(i.PostTagCommits)))
+		if i.Dirty {
+			build.WriteString(".dirty")
+		}
+		i.Build = build.String()
+		sb.WriteString(fmt.Sprintf("+%v", i.Build))
+	}
+
+	i.short = sb.String()
+	if semver.IsValid(i.short) {
+		// DON'T DO THIS. It strips the prerelease and build info.
+		// i.short = semver.Canonical(i.short)
+		// Do this instead.
+		canonical = semver.Canonical(i.short)
+		// Numeric versions...
+		if n, err = strconv.Atoi(strings.TrimPrefix(semver.Major(canonical), "v")); err != nil {
+			err = nil
+		} else {
+			i.Major = uint(n)
+		}
+		if n, err = strconv.Atoi(strings.Split(semver.MajorMinor(canonical), ".")[1]); err != nil {
+			err = nil
+		} else {
+			i.Minor = uint(n)
+		}
+		if n, err = strconv.Atoi(patchReIsolated.FindStringSubmatch(strings.Split(canonical, ".")[2])[1]); err != nil {
+			err = nil
+		} else {
+			i.Patch = uint(n)
+		}
+		// The other tag assignments were performed above.
+	}
+	// The default is 0 for the numerics, so no big deal.
+
+	i.isDefined = true
+
+	b = &i
+
+	return
+}
diff --git a/version/funcs_buildinfo.go b/version/funcs_buildinfo.go
new file mode 100644
index 0000000..0d27cdf
--- /dev/null
+++ b/version/funcs_buildinfo.go
@@ -0,0 +1,103 @@
+package version
+
+import (
+	"fmt"
+	"strings"
+
+	"golang.org/x/mod/semver"
+)
+
+// Detail returns a multiline string containing every possible piece of information we collect.
+func (b *BuildInfo) Detail() (ver string) {
+
+	var sb strings.Builder
+
+	sb.WriteString(fmt.Sprintf("%v\n\n", b.short))
+	sb.WriteString(fmt.Sprintf("====\nSource Control: %v\n", b.SourceControl))
+	if b.TagVersion != "" {
+		if b.PostTagCommits > 0 {
+			sb.WriteString(fmt.Sprintf("Version Base: %v\nCommit Hash: %v\n", b.TagVersion, b.CommitHash))
+		} else {
+			sb.WriteString(fmt.Sprintf("Version: %v\n", b.TagVersion))
+		}
+	} else {
+		sb.WriteString(fmt.Sprintf("Version: (Unversioned)\nCommit Hash: %v\n", b.CommitHash))
+	}
+
+	// Post-commits
+	if b.TagVersion != "" {
+		sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.TagVersion, b.PostTagCommits))
+	} else {
+		sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.CommitId, b.PostTagCommits))
+	}
+
+	sb.WriteString("Uncommitted/Unstaged Changes: ")
+	if b.Dirty {
+		sb.WriteString("yes (dirty/monkeypatched build)\n")
+	} else {
+		sb.WriteString("no (clean build)\n")
+	}
+
+	if b.TagVersion != "" {
+		sb.WriteString(
+			fmt.Sprintf(
+				"====\nMajor: %v\nMinor: %v\nPatch: %v\n",
+				b.Major, b.Minor, b.Patch,
+			),
+		)
+	}
+	sb.WriteString("====\n")
+	sb.WriteString(b.Meta())
+
+	ver = sb.String()
+
+	return
+}
+
+// Short returns a uniquely identifiable version string.
+func (b *BuildInfo) Short() (ver string) {
+
+	ver = b.short
+
+	return
+}
+
+// Meta returns the build/compile-time info.
+func (b *BuildInfo) Meta() (meta string) {
+
+	var sb strings.Builder
+
+	if b.RealBuildUser != b.BuildUser && b.RealBuildUser != "" {
+		sb.WriteString(fmt.Sprintf("Real Build User: %v\n", b.RealBuildUser))
+		sb.WriteString(fmt.Sprintf("Sudo Build User: %v\n", b.BuildUser))
+	} else {
+		sb.WriteString(fmt.Sprintf("Build User: %v\n", b.BuildUser))
+	}
+	sb.WriteString(fmt.Sprintf("Build Time: %v\nBuild Host: %v\n", b.BuildTime, b.BuildHost))
+
+	meta = sb.String()
+
+	return
+}
+
+// getReMap gets a regex map of map[pattern]match.
+func (b *BuildInfo) getReMap() (matches map[string]string) {
+
+	var s string = b.Short()
+	var sections []string
+
+	if !semver.IsValid(s) {
+		return
+	}
+
+	sections = strings.Split(s, ".")
+
+	// The split should contain everything in the third element.
+	// Or, if using a "simplified" semver, the last element.
+	matches = make(map[string]string)
+	for idx, str := range patchRe.FindStringSubmatch(sections[len(sections)-1]) {
+		matches[patchRe.SubexpNames()[idx]] = str
+	}
+
+	return
+}
diff --git a/version/types.go b/version/types.go
new file mode 100644
index 0000000..084e51d
--- /dev/null
+++ b/version/types.go
@@ -0,0 +1,51 @@
+package version
+
+import (
+	"time"
+)
+
+// BuildInfo contains nativized version information.
+type BuildInfo struct {
+	// TagVersion is the most recent tag name on the current branch.
+	TagVersion string
+	// PostTagCommits is the number of commits after BuildInfo.TagVersion's commit on the current branch.
+	PostTagCommits uint
+	// CommitHash is the full commit hash.
+	CommitHash string
+	// CommitId is the "short" version of BuildInfo.CommitHash.
+	CommitId string
+	// BuildUser is the user the program was compiled under.
+	BuildUser string
+	// If compiled under sudo, BuildInfo.RealBuildUser is the user that called sudo.
+	RealBuildUser string
+	// BuildTime is the time and date of the program's build time.
+	BuildTime time.Time
+	// BuildHost is the host the binary was compiled on.
+	BuildHost string
+	// Dirty specifies if the source was "dirty" (uncommitted/unstaged etc. files) at the time of compilation.
+	Dirty bool
+	// SourceControl is the source control version used. Only relevant if not a "clean" build or untagged.
+	SourceControl string
+	// Major is the major version, expressed as an uint per spec.
+	Major uint
+	// Minor is the minor version, expressed as an uint per spec.
+	Minor uint
+	// Patch is the patch version, expressed as an uint per spec.
+	Patch uint
+	// Pre
+	Pre string
+	// Build
+	Build string
+	// isDefined specifies if this version was retrieved from the built-in values.
+	isDefined bool
+	// raw is the raw variable values.
+	raw map[string]string
+	/*
+		verSplit is a slice of []string{Major, Minor, Patch, PreRelease, Build}
+
+		If using an actual point release, PreRelease and Build are probably blank.
+	*/
+	verSplit [5]string
+	// short is the condensed version of verSplit.
+	short string
+}