initial commit

This commit is contained in:
brent saner 2024-12-17 17:39:10 -05:00
commit 010643757e
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
29 changed files with 1644 additions and 0 deletions

49
.gitignore vendored Normal file
View File

@ -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/

87
build.sh Executable file
View File

@ -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."

View File

@ -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;

15
cachedb/consts.go Normal file
View File

@ -0,0 +1,15 @@
package cachedb

import (
_ "embed"
)

var (
//go:embed "_static/cache.schema.sql"
schemaBytes []byte
)

const (
SelectTunnels string = ""
SelectTunnelById string = ""
)

35
cachedb/funcs.go Normal file
View File

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

27
cachedb/types.go Normal file
View File

@ -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"`
}

1
cmd/gobroke/main.go Normal file
View File

@ -0,0 +1 @@
package gobroke

82
conf/_testdata/test.json Normal file
View File

@ -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"
}
]
}

289
conf/_testdata/test.toml Normal file
View File

@ -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=<TunnelID>
# 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"

68
conf/_testdata/test.xml Normal file
View File

@ -0,0 +1,68 @@
<!--
See the example TOML for detailed comments and explanations.
-->
<config defaultUser="default_user"
oneTun="true"
cacheDb="/var/cache/gobroke.db">
<cachePerms>
<file user=""
group=""
mode="384"/>
<dir user=""
group=""
mode="448"/>
</cachePerms>
<tunnels>
<tunnel id="123"
addr="203.0.113.1"
mtu="1450"
username="specific_user"
key="abcdef">
<config>
<tpl 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"/>
</perms>
<cmds>
<cmd bin="/usr/local/bin/somecmd"
isol8Env="false"
onChange="true"
isTpl="false">
<args>
<arg>-f</arg>
<arg>foo</arg>
</args>
<envs>
<env>SOMEENV=SOMEVAL</env>
</envs>
</cmd>
</cmds>
</tpl>
<tpl tpl="/etc/gobroke/tpl/stat.tpl"
dest="/tmp/gobroke.dump"/>
</config>
<commands>
<cmd bin="systemctl"
onChange="true">
<args>
<arg>restart</arg>
<arg>someservice</arg>
</args>
</cmd>
</commands>
</tunnel>
<tunnel id="456"
mtu="1480"
username="specific_user"
key="defghi"/>
</tunnels>
<commands>
<cmd bin="/usr/local/bin/alltunsprogram" isol8Env="false"/>
</commands>
</config>

59
conf/_testdata/test.yml Normal file
View File

@ -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

9
conf/consts.go Normal file
View File

@ -0,0 +1,9 @@
package conf

import (
"github.com/go-playground/validator/v10"
)

var (
validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled())
)

10
conf/errs.go Normal file
View File

@ -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")
)

75
conf/funcs.go Normal file
View File

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

50
conf/funcs_test.go Normal file
View File

@ -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))

}

1
conf/funcs_tunnel.go Normal file
View File

@ -0,0 +1 @@
package conf

129
conf/types.go Normal file
View File

@ -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=<TunnelID>
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"`
}

27
go.mod Normal file
View File

@ -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
)

61
go.sum Normal file
View File

@ -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=

34
tplCmd/funcs_cmd.go Normal file
View File

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

View File

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

21
tplCmd/types.go Normal file
View File

@ -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"`
}

23
tunnelbroker/consts.go Normal file
View File

@ -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"
)

9
tunnelbroker/funcs.go Normal file
View File

@ -0,0 +1,9 @@
package tunnelbroker

// NewClient reuturns a Client.
func NewClient() (c *Client, err error) {

// TODO

return
}

49
tunnelbroker/types.go Normal file
View File

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

32
version/consts.go Normal file
View File

@ -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<patch>[0-9+])(?P<pre>-[0-9A-Za-z.-]+)?(?P<build>\+[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

144
version/funcs.go Normal file
View File

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

103
version/funcs_buildinfo.go Normal file
View File

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

51
version/types.go Normal file
View File

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