update... work pending

This commit is contained in:
brent saner 2025-02-04 12:14:08 -05:00
parent 3b4d712722
commit 3c984a0636
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
39 changed files with 2122 additions and 597 deletions

View File

@ -0,0 +1,70 @@
{
"default_username": "default_user",
"freq": "5m",
"1tun": true,
"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"
}
]
}

View File

@ -0,0 +1,280 @@
# This file is heavily commented explaining various configuration options.
# The other configuration file examples are uncommented, but their field names
# should be easily visually mapped to the ones in here.
# All example configuration files evaluate to the same configuration.
# The uncommented.toml file is the exact same is this but without
# empty newlines and comments.

# 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'

# Frequency specifies the check frequency in daemon mode.
# (Not used in single-run mode.)
# If not specified, it defaults to 5 minutes.
# Note that this does not specify how often the client IP is set/updated
# upstream, it specifies how often to *check* if it needs to be updated.
# It will always attempt to be updated if there is a mismatch, this just
# controls how often that check runs.
# It must be a string compatible with Golang's time.ParseDuration.
# (https://pkg.go.dev/time#ParseDuration)
# Note that there may be some "drift" from this; the timer is restarted
# after a check/update *completes* to avoid duplicate job duplication.
# Likewise, if SingleTunnel (below) is true, each run may take even
# longer than expected.
Frequency = '5m'

# 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


#############
## 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.
# IMPORTANT: *DO NOT* define multiple tunnels with the same TunnelID.
# It makes no sense to do so, and GoBroke assumes that there are no
# duplicates.
[[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.
# This is not used by anything directly in GoBroke, but is contained here
# to assist in templating that may be configured.
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
[[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 this library's definition of the runner.TunnelResult 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 Destination 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.
# 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.
[[Tunnel.ConfigTemplate.Permissions]]
# Permissions are/may be defined for both the file being written
# and the parent directory (see below).
[[Tunnel.ConfigTemplate.Permissions.File]]
# The User is optional.
# If specified as '-1', the owner will not be modified/enforced.
# If specified as an empty string (the default), the runtime EUID is enforced.
# Otherwise, it may be a username or a UID (checked in that order).
# (For new files/directories, the OS default behavior is used.)
User = ""
# Group is also optional, and follows the same exact logic as User 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 0-4095), 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/includes "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
# Note that this does, technically, work on Windows but only read vs. read-write
# for the User is used (https://pkg.go.dev/os?GOOS=windows#Chmod).
# If not specified, the default is 0o0600 for files and 0o0700 for directories.
Mode = 0o0600
# Dir permissions specifiy permissions/ownership of the parent directory of File.
# The same rules, logic, behavior, etc. as in File apply here.
[[Tunnel.ConfigTemplate.Permissions.Dir]]
User = ""
Group = ""
Mode = 0o0700


##############################
## Config Template Commands ##
##############################

# Commands are am optional 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.
# Writing out this template to disk as a new file counts as a "change".
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 runner.TunnelResult.
# 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"

View File

@ -0,0 +1,60 @@
<!--
See the example TOML for detailed comments and explanations.
-->
<config defaultUser="default_user"
freq="5m"
oneTun="true">
<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>

View File

@ -0,0 +1,49 @@
# See the example TOML for detailed comments and explanations.
Default Username: default_user

Frequency: 5m

Single Tunnel: true

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

View File

@ -1,15 +1,6 @@
DefaultUsername = "default_user" DefaultUsername = 'default_user'
Frequency = '5m'
SingleTunnel = true SingleTunnel = true
CacheDbPath = '/var/cache/gobroke.db'
[CacheDbPerms]
[CacheDbPerms.File]
User = ""
Group = ""
Mode = 0o0600
[CacheDbPerms.Dir]
User = ""
Group = ""
Mode = 0o0700
[[Tunnel]] [[Tunnel]]
TunnelID = 123 TunnelID = 123
ExplicitClientIP = '203.0.113.1' ExplicitClientIP = '203.0.113.1'

View File

@ -0,0 +1,118 @@
{{- /*gotype: r00t2.io/gobroke/runner.TunnelResult*/ -}}
{{- $res := . -}}
{{- /*
In addition to all functions from net, net/netip, and go4.org/netipx
(with the exceptions of functions duplicated by methods which can be used by objects
returned from the above-mentioned functions),
the sprig func map (https://masterminds.github.io/sprig/) is also available.
*/ -}}
{{- /*
Data
*/ -}}
{{- /*
Arbitrary data may be assigned as JSON within the template, and parsed in using the fromJson function.
*/ -}}
{{- $dataMap := fromJson `{"enp1s0": {"tag": "wan"}}` -}}
{{- /*
Or explicitly created via sprig.
*/ -}}
{{- $wan_ifaces := splitList "," "enp1s0,enp2s0" -}}
{{- /*
Or via the various networking functionality.
*/ -}}
{{- $pfx := $res.TunnelAfter.Routed64 -}}
{{- if $res.TunnelAfter.Has48 -}}
{{- $pfx = $res.TunnelAfter.Routed48 }}
{{- end -}}
{{- /*
Settings
*/ -}}
{{- $v4_wan := -}}
{{- /* SLAAC */ -}}
{{- /*
Maximum seconds allowed between sending unsolicited multicast RAs. 4 < x < 1800
If using Mobile Extensions, 0.07 < x 1800
*/ -}}
{{- $max_inter := 60 -}}
{{- /*
Minimum seconds allowed between sending unsolicited multicast RAs. 3 < x < (0.75 * max_inter)
If using Mobile Extensions, 0.33 < x (e.g. 0.75 * max_inter)
*/ -}}
{{- $min_inter := 10 -}}
{{- /*
Minimum seconds between sending multicast RAs (solicited and unsolicited).
If using Mobile Extensions, 0.03 < x
*/ -}}
{{- $min_delay := 3 -}}
{{- /*
The lifetime associated with the default router in units of seconds. 0 OR max_inter < x < 9000
*/ -}}
{{- $lifetime := 9000 -}}
{{- /* DHCPv6 */ -}}
{{- /*
How long the lease should last until a new one is requested.
*/ -}}
{{- $lease_life := 21600 -}}{{- /* 6 hours == 21600 seconds */ -}}
{{- /*
How long the options are valid for.
It generally makes sense to align these with $lease_life.
It doesn't have to, but it's a good default most of the time.
*/ -}}
{{- $opts_life := $lease_life -}}
{{-/*
Config
*/-}}
# This file should be *included* in your dnsmasq configuration.
# Generated by GoBroke.
# See "dnsmasq --help dhcp6" for matching option identifers ("dhcp-option = ..., option6: <option>").
enable-ra
{{- range $pfxIdx, $pfx := }}
{{- set assign_loop = loop -}}
{%- set ra_opts = [] -%}
{%- if assignment.ra_tag -%}
{%- set id_set = 'tag:' + assignment.ra_tag -%}
{%- set identifier = id_set -%}
{%- set do_listen = false -%}
{%- else -%}
{%- set id_set = 'set:' + assignment.iface -%}
{%- set identifier = 'tag:' + assignment.iface -%}
{%- set do_listen = true -%}
{%- endif -%}
{%- if assignment.ra_dhcp is false -%}
{%- do ra_opts.append('ra-only') -%}
{%- if assignment.ra_other is true -%}
{%- do ra_opts.append('ra-stateless') -%}
{%- endif -%}
{%- endif -%}
{%- do ra_opts.append('slaac') -%}
{%- do ra_opts.append('ra-names') -%}
# {{ assignment.iface }} assignment
# Assignment blocks:
{%- for b in assignment.iface_blocks %}
# * {{ b|string }}
{%- endfor %}
{%- if do_listen %}
listen-address = {{ assignment.iface_ll }}
{%- endif %}
ra-param = {{ assignment.iface }}, mtu:{{ common_opts.mtu }}, high, {{ common_opts.min_delay }}, {{ common_opts.lifetime }}
{%- if assignment.ra_dhcp %}
{%- for block in assignment.assign_objs %}
{%- set dhcp_range = block.dhcp6_range|join(', ') -%}
{%- if loop.index0 == 0 %}
dhcp-range = {{ id_set }}, {{ dhcp_range }}, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}
{%- else %}
dhcp-range = {{ identifier }}, {{ dhcp_range }}, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}
{%- endif %}
{%- endfor %}
{%- else %}
dhcp-range = {{ id_set }}, ::, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}{#- TODO: check this. #}
{%- endif %}
dhcp-option = {{ identifier }}, option6:information-refresh-time, {{ common_opts.opts_life }}
{%- if assignment.ra_dns %}
dhcp-option = {{ identifier }}, option6:dns-server, [{{ assignment.iface_ll }}]
{%- endif %}
{%- if assignment.ra_domains %}
dhcp-option = {{ identifier }}, option6:domain-search, {{ assignment.ra_domains|join(',') }}
{%- endif %}

{% endfor %}

View File

@ -1,43 +0,0 @@
PRAGMA foreign_keys= OFF;
PRAGMA journal_mode = WAL;
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 TIMESTAMP NOT NULL,
checked TIMESTAMP NOT NULL,
updated TIMESTAMP
);
CREATE TABLE client_ips
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
tun_id INTEGER NOT NULL,
client_ip INTEGER NOT NULL,
when_set TIMESTAMP NOT NULL,
when_fetched TIMESTAMP,
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);
CREATE TABLE metadata
(
key TEXT NOT NULL,
value TEXT,
created TIMESTAMP NOT NULL,
updated TIMESTAMP
);
COMMIT;
PRAGMA foreign_keys= ON;

View File

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

import (
_ "embed"
)

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

View File

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

import (
`os`
`path/filepath`

`github.com/jmoiron/sqlx`
_ `github.com/mattn/go-sqlite3`
`r00t2.io/gobroke/conf`
`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, perms *conf.Perms) (c *Cache, err error) {

var cache Cache
var exists bool

switch db {
case ":memory:":
// NO-OP for now; exists should be false, but it is since it's zero-val.
default:
if perms == nil {
perms = new(conf.Perms)
if err = perms.SetMissing(); err != nil {
return
}
}
if exists, err = paths.RealPathExists(&db); err != nil {
return
}
if !exists {
if err = os.MkdirAll(filepath.Dir(db), *perms.ParentDir.Mode); err != nil {
return
}
if err = os.WriteFile(db, nil, *perms.File.Mode); err != nil {
return
}
}
if err = perms.Chown(filepath.Dir(db)); err != nil {
return
}
if err = perms.Chmod(filepath.Dir(db), !exists); err != nil {
return
}
if err = perms.Chown(db); err != nil {
return
}
if err = perms.Chmod(db, !exists); err != nil {
return
}
}

if cache.db, err = sqlx.Connect("sqlite3", db); err != nil {
return
}

if !exists {
// New DB, so write the schema.
if _, err = cache.db.Exec(string(schemaBytes)); err != nil {
return
}

}

c = &cache

return
}

View File

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

// Close closes a Cache. This should be called when done using it.
func (c *Cache) Close() (err error) {

if c.db != nil {
if err = c.db.Close(); err != nil {
return
}
}

return
}

View File

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

import (
`net`
`time`

`github.com/jmoiron/sqlx`
`r00t2.io/gobroke/tunnelbroker`
)

/*
Cache is used to access a database holding historical tunnel configuration and update information.
This is primarily used to keep request load low and update load even lower to tunnelbroker.net servers.
*/
type Cache struct {
db *sqlx.DB
}

/*
DbTunnel is a cached tunnelbroker.Tunnel paired with a conf.Tunnel checksum.
It is used to compare defined tunnels on restart, and can be used to fetch last-known usptream configuration.
If the CRC32 mismatches, an update will be forced.
Otherwise it is subject to normal frequency rates for checking.
*/
type DbTunnel struct {
*tunnelbroker.Tunnel
// CRC32 is a checksum of *the associated conf.Tunnel*, NOT the upstream tunnel configuration.
CRC32 uint32 `db:"cksum_crc32"`
// Created is when this *row* is created.
Created time.Time `db:"created"`
// Checked is when the client IP was last checked against the live configuration at tunnelbroker.net.
Checked time.Time `db:"checked"`
// Updated is when the client IP was last updated at tunnelbroker.net (by GoBroke), *or* the Tunnel changed.
Updated time.Time `db:"updated"`
}

/*
DbClientIP contains a row describing a result of a client IP fetch from a dynamic service (e.g. https://c4.r00t2.io/).
If an explicit address is provided for a conf.Tunnel, there will not be a corresponding row for it.
This is only a history of publicly fetched IPs.
*/
type DbClientIP struct {
// ID is a row identifier serving as primary key; it doesn't have any particular significance beyond that.
ID uint `db:"id"`
// TunID corresponds to a DbTunnel.Tunnel.ID. It is foreign keyed against it.
TunID uint `db:"tun_id"`
// ClientIPv4 is the client-end registered for tunnel TunID.
ClientIPv4 net.IP `db:"client_ip"`
// Set specifies when the IP was last set at tunnelbroker.net.
Set time.Time `db:"when_set"`
// Fetched specifies when the client IP was last *fetched* from a remote service.
Fetched time.Time `db:"when_fetched"`
}

10
cmd/gobroke/args.go Normal file
View File

@ -0,0 +1,10 @@
package main

type Args struct {
Version bool `short:"v" long:"version" description:"Print the version and exit."`
DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."`
DoDebug bool `env:"GOBROKE_DEBUG" short:"d" long:"debug" description:"If specified, enable debug logging. This may log potentially sensitive information, so be careful."`
DryRun bool `env:"GOBROKE_RO" short:"n" long:"dry-run" description:"If specified, only perfrom dry-run operations (read-only). Changes that would be made are logged but are not actually performed."`
ConfigPath string `env:"GOBROKE_CFG" required:"true" short:"c" long:"config" default:"/etc/gobroke/gobroke.conf" description:"The path to the configuration file." validate:"required,file"`
Daemon bool `env:"GOBROKE_DAEMON" short:"D" long:"daemon" description:"Run as a daemon. If not specified, GoBroke will run once, updating the tunnel configuration if necessary, and then quit."`
}

10
cmd/gobroke/consts.go Normal file
View File

@ -0,0 +1,10 @@
package main

import (
`log`
)

const (
logFlags int = log.LstdFlags | log.Lmsgprefix
logFlagsDebug int = logFlags | log.Llongfile
)

View File

@ -0,0 +1,21 @@
//go:build !windows

package main

import (
`r00t2.io/goutils/logging`
)

// getOsLogger adds the default logger per OS.
func getOsLogger(logger *logging.MultiLogger, logFlagsRuntime int) (err error) {

if err = logger.AddDefaultLogger(
"default",
logFlagsRuntime,
"/var/log/gobroke/gobroke.log", "~/logs/gobroke.log",
); err != nil {
return
}

return
}

View File

@ -0,0 +1,17 @@
//go:build windows

package main

import (
`r00t2.io/goutils/logging`
)

// getOsLogger adds the default logger per OS.
func getOsLogger(logger *logging.MultiLogger, logFlagsRuntime int) (err error) {

if err = logger.AddDefaultLogger("default", logging.DefaultEventID, logFlagsRuntime, `C:\GoBroke\GoBroke.log`, `~\GoBroke\GoBroke.log`); err != nil {
return
}

return
}

View File

@ -1 +1,106 @@
package gobroke package main

import (
`errors`
`fmt`
`log`
`os`

`github.com/davecgh/go-spew/spew`
`github.com/jessevdk/go-flags`
`r00t2.io/gobroke/conf`
`r00t2.io/gobroke/daemon`
`r00t2.io/gobroke/runner`
`r00t2.io/gobroke/version`
`r00t2.io/goutils/logging`
)

func main() {

var err error
var args *Args = new(Args)
var logger *logging.MultiLogger
var cfg *conf.Config
var logFlagsRuntime int = logFlags
var singleResults []*runner.TunnelResult
var tunsChanged bool
var tunsUpdated bool
var updater *daemon.Updater
var flagsErr *flags.Error = new(flags.Error)
var parser *flags.Parser = flags.NewParser(args, flags.Default)

if _, err = parser.Parse(); err != nil {
switch {
case errors.As(err, &flagsErr):
switch {
case errors.Is(flagsErr.Type, flags.ErrHelp), errors.Is(flagsErr.Type, flags.ErrCommandRequired), errors.Is(flagsErr.Type, flags.ErrRequired): // These print their relevant messages by themselves.
return
default:
log.Panicln(err)
}
default:
log.Panicln(err)
}
}

if version.Ver, err = version.Version(); err != nil {
log.Panicln(err)
}

// If args.Version or args.DetailVersion are true, just print them and exit.
if args.DetailVersion || args.Version {
if args.Version {
fmt.Println(version.Ver.Short())
return
} else if args.DetailVersion {
fmt.Println(version.Ver.Detail())
return
}
}

// We want to set up logging before anything else.
if args.DoDebug {
logFlagsRuntime = logFlagsDebug
}
logger = logging.GetMultiLogger(args.DoDebug, "GoBroke")
if err = getOsLogger(logger, logFlagsRuntime); err != nil {
log.Panicln(err)
}
if err = logger.Setup(); err != nil {
log.Panicln(err)
}
logger.Info("main: GoBroke version %v", version.Ver.Short())
logger.Debug("main: GoBroke version (extended):\n%v", version.Ver.Detail())
defer logger.Shutdown()

logger.Debug("Initialized with args:\n%v", spew.Sdump(args))

if cfg, err = conf.NewConfig(args.ConfigPath, args.DoDebug, logger); err != nil {
logger.Err("main: Received error while initializing config: %v", err)
log.Panicln(err)
}

if singleResults, tunsChanged, tunsUpdated, err = runner.Run(cfg, logger); err != nil {
logger.Err("main: Received error while running startup checks/updates: %v", err)
log.Panicln(err)
}
logger.Info("main: Startup check: Changed: %v, Updated: %v", tunsChanged, tunsUpdated)
logger.Debug("main: Startup check: Tunnel results:\n%s", spew.Sdump(singleResults))

if !args.Daemon {
// Single run.
logger.Debug("main: Done.")
os.Exit(0)
}

if updater, err = daemon.NewUpdater(cfg, logger); err != nil {
logger.Err("main: Received error while initializing persistent updater: %v", err)
log.Panicln(err)
}
if err = updater.Start(); err != nil {
logger.Err("main: Received error while starting/running persistent updater: %v", err)
log.Panicln(err)
}

logger.Debug("main: Done.")
}

View File

@ -1,19 +1,7 @@
{ {
"default_username": "default_user", "default_username": "default_user",
"freq": "5m",
"1tun": true, "1tun": true,
"cache_db": "/var/cache/gobroke.db",
"cache_perms": {
"file": {
"user": "",
"group": "",
"mode": 384
},
"dir": {
"user": "",
"group": "",
"mode": 448
}
},
"tunnels": [ "tunnels": [
{ {
"tun_id": 123, "tun_id": 123,

View File

@ -1,189 +1,15 @@
# This file is heavily commented explaining various configuration options. DefaultUsername = 'default_user'
# The other configuration file examples are uncommented, but their field names Frequency = '5m'
# should be easily visually mapped to the ones in here.
# All example configuration files evaluate to the same configuration.
# The test_uncommented.toml file is the exact same is this but without
# empty newlines and comments.

# 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 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 specified as '-1', the owner will not be modified/enforced.
# If specified as an empty string (the default), the runtime EUID is enforced.
# Otherwise, it may be a username or a UID (checked in that order).
# (For new files/directories, the OS default behavior is used.)
User = ""
# Group is also optional, and follows the same exact logic as User 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 0-4095), 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/includes "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
# Note that this does, technically, work on Windows but only read vs. read-write
# for the User is used (https://pkg.go.dev/os?GOOS=windows#Chmod).
# If not specified, the default is 0o0600 for files and 0o0700 for directories.
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]] [[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 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' 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 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" 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" 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]] [[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" 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" 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]]
[[Tunnel.ConfigTemplate.Permissions.File]] [[Tunnel.ConfigTemplate.Permissions.File]]
User = "" User = ""
@ -193,108 +19,30 @@ CacheDbPath = '/var/cache/gobroke.db'
User = "" User = ""
Group = "" Group = ""
Mode = 0o0700 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]] [[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' ProgramPath = '/usr/local/bin/somecmd'
# Args are optional for a Command.
# They should conform to the rules for (os/)exec.Cmd.Args.
Args = [ Args = [
'-f', 'foo', '-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 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 = [ EnvVars = [
'SOMEENV=SOMEVAL', '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 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 IsTemplate = false

#######################################################################

# Multiple ConfigTemplates may be specified.
[[Tunnel.ConfigTemplate]] [[Tunnel.ConfigTemplate]]
Template = "/etc/gobroke/tpl/stat.tpl" Template = "/etc/gobroke/tpl/stat.tpl"
Destination = "/tmp/gobroke.dump" 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]] [[Tunnel.Command]]
ProgramPath = 'systemctl' ProgramPath = 'systemctl'
Args = [ Args = [
'restart', 'restart',
'someservice', '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 OnChange = true

###############################################################################

# Multiple tunnel configurations are supported as well.
[[Tunnel]] [[Tunnel]]
TunnelID = 456 TunnelID = 456
Username = "specific_user" Username = "specific_user"
UpdateKey = "defghi" 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]] [[Command]]
ProgramPath = "/usr/local/bin/alltunpsrogram" ProgramPath = "/usr/local/bin/alltunpsrogram"

View File

@ -1,17 +1,6 @@
<!--
See the example TOML for detailed comments and explanations.
-->
<config defaultUser="default_user" <config defaultUser="default_user"
oneTun="true" freq="5m"
cacheDb="/var/cache/gobroke.db"> oneTun="true">
<cachePerms>
<file user=""
group=""
mode="384"/>
<dir user=""
group=""
mode="448"/>
</cachePerms>
<tunnels> <tunnels>
<tunnel id="123" <tunnel id="123"
addr="203.0.113.1" addr="203.0.113.1"

View File

@ -1,19 +1,8 @@
# See the example TOML for detailed comments and explanations.
Default Username: default_user Default Username: default_user


NoGoTunnel: true Frequency: 5m


Cache Database Path: /var/cache/gobroke.db Single Tunnel: true

Cache Database Permissions:
File:
User: ''
Group: ''
Mode: 384
Directory:
User: ''
Group: ''
Mode: 448


Tunnels: Tunnels:
- Tunnel ID: 123 - Tunnel ID: 123

View File

@ -2,69 +2,151 @@ package conf


import ( import (
`encoding/json` `encoding/json`
`encoding/xml`
`hash`
`os`


"github.com/BurntSushi/toml" `github.com/BurntSushi/toml`
"github.com/creasty/defaults" `github.com/creasty/defaults`
"github.com/goccy/go-yaml" `github.com/davecgh/go-spew/spew`
"r00t2.io/sysutils/paths" `github.com/goccy/go-yaml`
`github.com/zeebo/blake3`
`r00t2.io/gobroke/tplCmd`
`r00t2.io/goutils/logging`
`r00t2.io/sysutils/paths`
) )


// NewConfig returns a conf.Config from filepath path. /*
func NewConfig(path string) (cfg *Config, err error) { Checksum guarantees a standard checksum of a configuration.
If you are comparing new vs. old configs, be sure to use this function
and compare against a Config.Checksum() to ensure consistent hashing algorithms etc.
*/
func Checksum(confBytes []byte) (cksum []byte, err error) {


var b []byte // If built with CGO enabled, this'll take advantage of SIMD.
// Should be fast enough without it, though.
var h hash.Hash = blake3.New()


if err = paths.RealPath(&path); err != nil { if _, err = h.Write(confBytes); err != nil {
return return
} }


if cfg, err = NewConfigFromBytes(b); err != nil { cksum = h.Sum(nil)

return
}

/*
ChecksumPath is a convenience wrapper around Checksum, operating on a filepath instead of bytes.
*/
func ChecksumPath(confPath string) (cksum []byte, err error) {

var b []byte

if err = paths.RealPath(&confPath); err != nil {
return
}

if b, err = os.ReadFile(confPath); err != nil {
return
}

if cksum, err = Checksum(b); err != nil {
return
}

return
}

// NewConfig returns a conf.Config from filepath path.
func NewConfig(path string, debug bool, log logging.Logger) (cfg *Config, err error) {

var b []byte

if log == nil {
log = &logging.NullLogger{}
}

log.Debug("conf.NewConfig: New config from path '%s'", path)

if err = paths.RealPath(&path); err != nil {
log.Err("conf.NewConfig: Received error canonizing config path '%s': %s", path, err)
return
}
log.Debug("conf.NewConfig: Canonized configuration path to '%s'", path)

if cfg, err = NewConfigFromBytes(b, debug, log); err != nil {
log.Err("conf.NewConfig: Received error parsing config '%s': %s", path, err)
return return
} }
cfg.confPath = new(string) cfg.confPath = new(string)
*cfg.confPath = path *cfg.confPath = path


log.Debug("conf.NewConfig: Successfully parsed and loaded configuration '%s'", path)

return return
} }


// NewConfigFromBytes returns a conf.Config from bytes b. b may be a JSON, TOML, XML, or YAML representation. // NewConfigFromBytes returns a conf.Config from bytes b. b may be a JSON, TOML, XML, or YAML representation.
func NewConfigFromBytes(b []byte) (cfg *Config, err error) { func NewConfigFromBytes(b []byte, debug bool, log logging.Logger) (cfg *Config, err error) {


if err = json.Unmarshal(b, &cfg); err != nil { var tplBytes []byte
if err = yaml.Unmarshal(b, &cfg); err != nil {
if err = toml.Unmarshal(b, &cfg); err != nil { if b == nil || len(b) == 0 {
if err = toml.Unmarshal(b, &cfg); err != nil {
err = ErrUnkownSyntax
return return
} }

if log == nil {
log = &logging.NullLogger{}
} }

log.Debug("conf.NewConfigFromBytes: New config from %d bytes.", len(b))
log.Debug("conf.NewConfigFromBytes: Config before parsing:\n%s", string(b))

if err = json.Unmarshal(b, &cfg); err != nil {
if err = xml.Unmarshal(b, &cfg); err != nil {
if err = yaml.Unmarshal(b, &cfg); err != nil {
if err = toml.Unmarshal(b, &cfg); err != nil {
log.Err("conf.NewConfigFromBytes: Unable to parse config as JSON, XML, YAML, or TOML; config invalid.")
err = ErrUnkownSyntax
return
} else {
log.Debug("conf.NewConfigFromBytes: Config parsed as TOML.")
} }
} else {
log.Debug("conf.NewConfigFromBytes: Config parsed as YAML.")
}
} else {
log.Debug("conf.NewConfigFromBytes: Config parsed as XML.")
}
} else {
log.Debug("conf.NewConfigFromBytes: Config parsed as JSON.")
} }


if err = defaults.Set(cfg); err != nil { if err = defaults.Set(cfg); err != nil {
return return
} }

log.Debug("conf.NewConfigFromBytes: Configuration after parsing and defaults:\n%s", spew.Sdump(cfg))
if cfg.CacheDB != ":memory:" {
if err = paths.RealPath(&cfg.CacheDB); err != nil {
return
}
if cfg.CacheDbPerms == nil {
cfg.CacheDbPerms = new(Perms)
}
if err = cfg.CacheDbPerms.SetMissing(); err != nil {
return
}
}


if err = validate.Struct(cfg); err != nil { if err = validate.Struct(cfg); err != nil {
log.Err("conf.NewConfigFromBytes: Config validation failed: %v", err)
return return
} }


cfg.log = log
cfg.debug = debug

for _, t := range cfg.Tunnels { for _, t := range cfg.Tunnels {
if t == nil {
continue
}
t.cfg = cfg t.cfg = cfg
if t.Username == nil { if t.Username == nil {
if cfg.Username == nil { if cfg.Username == nil {
log.Err(
"conf.NewConfigFromBytes: Username is not provided for tunnel %d and no default username was provided.",
t.TunnelID,
)
err = ErrMissingUser err = ErrMissingUser
return return
} else { } else {
@ -73,14 +155,29 @@ func NewConfigFromBytes(b []byte) (cfg *Config, err error) {
} }
if t.TemplateConfigs != nil && len(t.TemplateConfigs) > 0 { if t.TemplateConfigs != nil && len(t.TemplateConfigs) > 0 {
for _, tpl := range t.TemplateConfigs { for _, tpl := range t.TemplateConfigs {
if tpl == nil {
continue
}
if err = paths.RealPath(&tpl.Template); err != nil { if err = paths.RealPath(&tpl.Template); err != nil {
log.Err("conf.NewConfigFromBytes: Unable to canonize path to template '%s': %s", tpl.Template, err)
return return
} }
if err = paths.RealPath(&tpl.Dest); err != nil { if err = paths.RealPath(&tpl.Dest); err != nil {
log.Err("conf.NewConfigFromBytes: Unable to canonize path to destination '%s': %s", tpl.Dest, err)
return
}
if tplBytes, err = os.ReadFile(tpl.Template); err != nil {
log.Err("conf.NewConfigFromBytes: Unable to read template file '%s': %s", tpl.Template, err)
return
}
tpl.Tpl = tplCmd.GetTpl()
if _, err = tpl.Tpl.Parse(string(tplBytes)); err != nil {
log.Err("conf.NewConfigFromBytes: Unable to parse template file '%s': %s", tpl.Template, err)
return return
} }
if tpl.Perms != nil { if tpl.Perms != nil {
if err = tpl.Perms.SetMissing(); err != nil { if err = tpl.Perms.SetMissing(); err != nil {
log.Err("conf.NewConfigFromBytes: Unable to enrich permissions for template '%s': %s", tpl.Template, err)
return return
} }
} }
@ -88,5 +185,12 @@ func NewConfigFromBytes(b []byte) (cfg *Config, err error) {
} }
} }


if cfg.cksum, err = Checksum(b); err != nil {
log.Err("conf.NewConfigFromBytes: Error calculating checksum: %s", err)
return
}

log.Debug("conf.NewConfigFromBytes: Successfully parsed and loaded configuration.")

return return
} }

31
conf/funcs_config.go Normal file
View File

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

// Checksum returns the current configuration's checksum.
func (c *Config) Checksum() (cksum []byte) {

cksum = make([]byte, len(c.cksum))
copy(cksum, c.cksum)

return
}

// IsDebug returns whether debug is enabled or not.
func (c *Config) IsDebug() (isDebug bool) {

isDebug = c.debug

return
}

/*
Path returns the config file this configuration was loaded from.
It'll be an empty string if it was loaded in directly from raw bytes.
*/
func (c *Config) Path() (path string) {

if c.confPath != nil {
path = *c.confPath
}

return
}

View File

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

// IsDebug returns whether debug is enabled or not.
func (t *Tunnel) IsDebug() (isDebug bool) {

isDebug = t.cfg.debug

return
}

View File

@ -5,8 +5,11 @@ import (
`io/fs` `io/fs`
`net` `net`
`os/user` `os/user`
`text/template`
`time`


`r00t2.io/gobroke/tplCmd` `r00t2.io/gobroke/tplCmd`
`r00t2.io/goutils/logging`
) )


// Config represents a configuration file. // Config represents a configuration file.
@ -21,20 +24,21 @@ type Config struct {
If not (and, of course, Config.Username is specified), then Config.Username will be used for that Tunnel. 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"` Username *string `json:"default_username,omitempty" toml:"DefaultUsername,omitempty" xml:"defaultUser,attr,omitempty" yaml:"Default Username,omitempty"`
// Freq indicates the (check, not update) frequency.
Freq time.Duration `json:"freq,omitempty" toml:"Frequency,omitempty" xml:"freq,attr,omitempty" yaml:"Frequency,omitempty" default:"5m" validate:"gt=0"`
// SingleTunnel, if true, will suppress goroutine-management of tunnels and instead execute them sequentially instead. // 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"` SingleTunnel bool `json:"1tun,omitempty" toml:"SingleTunnel,omitempty" xml:"oneTun,attr,omitempty" yaml:"Single Tunnel,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 contains one or more tunnel configurations.
Tunnels []*Tunnel `json:"tunnels" toml:"Tunnel" xml:"tunnels>tunnel" yaml:"Tunnels" validate:"required,dive,required"` Tunnels []*Tunnel `json:"tunnels" toml:"Tunnel" xml:"tunnels>tunnel" yaml:"Tunnels" validate:"required,dive,required"`
/* /*
Cmds are executed, in order, *after* all Tunnel configurations have been run. Cmds are executed, in order, *after* all Tunnel configurations have been run.
Unlike in Tunnel and ConfigTemplate, no templating on these commands is performed. 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" validate:"omitempty,dive"` Cmds []*tplCmd.Cmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"`
confPath *string confPath *string
debug bool
log logging.Logger
cksum []byte
} }


// Tunnel represents a single tunnel configuration from tunnelbroker.net. // Tunnel represents a single tunnel configuration from tunnelbroker.net.
@ -48,7 +52,7 @@ type Tunnel struct {
*/ */
TunnelID uint `json:"tun_id" toml:"TunnelID" xml:"id,attr" yaml:"Tunnel ID" validate:"required,ge=1"` 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. ExplicitAddr, if provided, will be used as the tunnelbroker.Tunnel.ClientIPv4 for tunnelbroker.Tunnel.Update.
If not provided, this will be fetched dynamically from an external source. 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" validate:"omitempty,ipv4"` ExplicitAddr *net.IP `json:"addr,omitempty" toml:"ExplicitClientIP,omitempty" xml:"addr,attr,omitempty" yaml:"Explicit Client IP Address,omitempty" validate:"omitempty,ipv4"`
@ -56,6 +60,7 @@ type Tunnel struct {
MTU should be specified if you have defined a custom one (under the "Advanced" tab for this tunnel at tunnlebroker.net). 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 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. on configuration parsing will reflect this.
This is not used by anything directly in GoBroke, but is contained here to assist in templating that may be configured.
*/ */
MTU uint `json:"mtu,omitempty" toml:"MTU,omitempty" xml:"mtu,attr,omitempty" yaml:"MTU,omitempty" default:"1480" validate:"required,gt=0,le=1480"` MTU uint `json:"mtu,omitempty" toml:"MTU,omitempty" xml:"mtu,attr,omitempty" yaml:"MTU,omitempty" default:"1480" validate:"required,gt=0,le=1480"`
/* /*
@ -71,14 +76,13 @@ type Tunnel struct {
*/ */
UpdateKey string `json:"update_key" toml:"UpdateKey" xml:"key,attr" yaml:"Update Key" validate:"required"` 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. // 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" validate:"omitempty,dive"` TemplateConfigs []*ConfigTemplate `json:"cfg_tpls" toml:"ConfigTemplate" xml:"config>tpl" yaml:"Configuration File Templates" validate:"omitempty,dive"`
/* /*
Cmds are executed, in order, *after* all tunnel updates/fetching and the templating has completed (if any specified). 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, Each command will also have runner.TunnelResult templated to it like TemplateConfigs/ConfigTemplate.Cmds,
so they may be templated as necessary. so they may be templated as necessary.
*/ */
Cmds []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"` Cmds []*tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"`
// cfg is the parent Config.
cfg *Config cfg *Config
} }


@ -95,17 +99,20 @@ type ConfigTemplate struct {
/* /*
Template is the path to the template file on disk. 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). 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. The struct passed to it is a runner.TunnelResult.
*/ */
Template string `json:"tpl" toml:"Template" xml:"tpl,attr" yaml:"Template File Path" validate:"required,filepath"` 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 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"` 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 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"` 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. // Cmds specifiies commands to run after this ConfigTemplate run.
Commands []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"cmds>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"` Cmds []*tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"cmds>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"`
// Tpl is the parsed template from Template.
Tpl *template.Template `json:"-" toml:"-" xml:"-" yaml:"-"`
} }


// Perms specify permissions for a file and its parent directory.
type Perms struct { type Perms struct {
// File specifies the desired permissions/ownership of the target file. // File specifies the desired permissions/ownership of the target file.
File *PermSpec `json:"file,omitempty" toml:"File,omitempty" xml:"file,omitempty" yaml:"File,omitempty"` File *PermSpec `json:"file,omitempty" toml:"File,omitempty" xml:"file,omitempty" yaml:"File,omitempty"`
@ -117,6 +124,7 @@ type Perms struct {
curGid int curGid int
} }


// PermSpec is used to define contextual permissions. It is used for both files and their parent directories.
type PermSpec struct { type PermSpec struct {
/* /*
User is the username or UID (tried in that order) to chown. User is the username or UID (tried in that order) to chown.

21
daemon/consts.go Normal file
View File

@ -0,0 +1,21 @@
package daemon

import (
`os`
`syscall`

sysdUtil `github.com/coreos/go-systemd/util`
)

// Signal traps
var (
stopSigs []os.Signal = []os.Signal{
syscall.SIGQUIT,
os.Interrupt,
syscall.SIGTERM,
}
reloadSigs []os.Signal = []os.Signal{
syscall.SIGHUP,
}
isSystemd bool = sysdUtil.IsRunningSystemd()
)

34
daemon/funcs.go Normal file
View File

@ -0,0 +1,34 @@
package daemon

import (
`os`
`time`

`r00t2.io/gobroke/conf`
`r00t2.io/goutils/logging`
)

// NewUpdater returns a new Updater.
func NewUpdater(cfg *conf.Config, log logging.Logger) (updater *Updater, err error) {

var u Updater = Updater{
cfg: cfg,
log: log,
doneChan: make(chan bool, 1),
stopChan: make(chan os.Signal),
reloadChan: make(chan os.Signal),
isStopping: false,
}

log.Debug("daemon.NewUpdater: Initializing new Updater.")

// This will start the timer immediately, but we restart it at the beginning of Updater.Start().
// It just shouldn't be nil.
u.timer = time.NewTimer(cfg.Freq)

updater = &u

log.Debug("daemon.NewUpdater: Updater initialized.")

return
}

177
daemon/funcs_updater.go Normal file
View File

@ -0,0 +1,177 @@
package daemon

import (
`bytes`
`os`
`time`

sysd "github.com/coreos/go-systemd/daemon"
`github.com/davecgh/go-spew/spew`
`r00t2.io/gobroke/conf`
`r00t2.io/gobroke/runner`
`r00t2.io/goutils/multierr`
)

// Start starts an Updater. This blocks.
func (u *Updater) Start() (err error) {

var sig os.Signal
var t time.Time
var tunResults []*runner.TunnelResult
var tunsChanged bool
var tunsUpdated bool
var mErr *multierr.MultiError = multierr.NewMultiError(nil)

u.log.Debug("daemon.Updater.Start: Starting persistent Updater.")

u.timer.Reset(u.cfg.Freq)

if isSystemd {
var supported bool

// https://www.freedesktop.org/software/systemd/man/sd_notify.html
if supported, err = sysd.SdNotify(false, sysd.SdNotifyReady); err != nil {
u.log.Err(
"daemon.Updater.Start: Error encountered when notifying systemd of changestate to READY (supported: %v): %v",
supported, err,
)
err = nil
}
}

breakLoop:
for !u.isStopping {
if u.isStopping {
break
}
select {
case t = <-u.timer.C:
u.log.Debug("daemon.Updater.Start: Tick at %s; running check/update.", t.String())
if tunResults, tunsChanged, tunsUpdated, err = runner.Run(u.cfg, u.log); err != nil {
u.log.Err("daemon.Updater.Start: Received error running check/update: %v", err)
mErr.AddError(err)
err = nil
} else {
u.log.Debug(
"daemon.Updater.Start: Check/update finished at %s; waiting %s. Changed: %v, Updated: %v, Results:\n%s",
t.String(), u.cfg.Freq.String(), tunsChanged, tunsUpdated, spew.Sdump(tunResults),
)
}
u.timer.Reset(u.cfg.Freq)
case sig = <-u.reloadChan:
u.log.Debug("daemon.Updater.Start: Received reload signal %v (%#v): %v", sig, sig, sig.String())
if u.isStopping {
break breakLoop
}
if err = u.Reload(); err != nil {
u.log.Err("daemon.Updater.Start: Received error running reload: %v", err)
mErr.AddError(err)
err = nil
}
case sig = <-u.stopChan:
u.isStopping = true
u.log.Debug("daemon.Updater.Start: Received stop signal %v (%#v): %v", sig, sig, sig.String())
if err = u.Stop(); err != nil {
u.log.Err("daemon.Updater.Start: Received error stopping: %v", err)
mErr.AddError(err)
err = nil
}
break breakLoop
}
}

if !mErr.IsEmpty() {
err = mErr
return
}

u.log.Debug("daemon.Updater.Start: Persistent updater stopped.")

return
}

// Stop stops an Updater. This is called by signal handlers in Start, and will close Start's block.
func (u *Updater) Stop() (err error) {

u.log.Debug("daemon.Updater.Stop: Stopping persistent Updater.")

if isSystemd {
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
if _, err = sysd.SdNotify(false, sysd.SdNotifyStopping); err != nil {
u.log.Err("daemon.Updater.Stop: Received error notifying systemd of stop: %v", err)
err = nil
}
}

u.isStopping = true
u.timer.Stop()
close(u.stopChan)
close(u.reloadChan)

u.log.Debug("daemon.Updater.Stop: Stopped persistent Updater.")

return
}

// Reload reloads the configuration if it was originally set via a file.
func (u *Updater) Reload() (err error) {

var prevCfg *conf.Config
var supported bool
var newCksum []byte

u.log.Debug("daemon.Updater.Reload: Reloading persistent Updater.")

if isSystemd {
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
if _, err = sysd.SdNotify(false, sysd.SdNotifyReloading); err != nil {
u.log.Err("daemon.Updater.Reload: Received error notifying systemd of reload: %v", err)
err = nil
}
}

if u.cfg.Path() != "" {
if newCksum, err = conf.ChecksumPath(u.cfg.Path()); err != nil {
u.log.Err(
"daemon.Updater.Reload: Received error getting checksum of defined conf path '%s'; skipping reload: %v",
u.cfg.Path(), err,
)
err = nil
} else {
if bytes.Equal(newCksum, u.cfg.Checksum()) {
u.log.Warning("daemon.Updater.Reload: Config path '%s' checksum unchanged; skipping reload", u.cfg.Path())
} else {
prevCfg = new(conf.Config)
*prevCfg = *u.cfg
if u.cfg, err = conf.NewConfig(u.cfg.Path(), u.cfg.IsDebug(), u.log); err != nil {
u.log.Err("daemon.Updater.Reload: Received error parsing new config; reverting to previous configuration: %v", err)
err = nil
u.cfg = prevCfg
} else {
u.log.Debug(
"daemon.Updater.Reload: New configuration loaded from '%s'; restarting timer for '%s'.",
u.cfg.Path(), u.cfg.Freq.String(),
)
u.timer.Reset(u.cfg.Freq)
}
}
}
} else {
u.log.Debug("daemon.Updater.Reload: No config filepath specified; NO-OP.")
}

if isSystemd {
// https://www.freedesktop.org/software/systemd/man/sd_notify.html
if supported, err = sysd.SdNotify(false, sysd.SdNotifyReady); err != nil {
u.log.Err(
"daemon.Updater.Reload: Error encountered when notifying systemd of changestate to READY (supported: %v): %v",
supported, err,
)
err = nil
}
}

u.log.Debug("daemon.Updater.Reload: Finished Updater reload.")

return
}

20
daemon/types.go Normal file
View File

@ -0,0 +1,20 @@
package daemon

import (
`os`
`time`

`r00t2.io/gobroke/conf`
`r00t2.io/goutils/logging`
)

// Updater runs a persistent checker/updater as a service/daemon.
type Updater struct {
cfg *conf.Config
log logging.Logger
timer *time.Timer
doneChan chan bool
stopChan chan os.Signal
reloadChan chan os.Signal
isStopping bool
}

29
go.mod
View File

@ -4,31 +4,46 @@ go 1.23.3


require ( require (
github.com/BurntSushi/toml v1.4.0 github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/chigopher/pathlib v0.19.1
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/creasty/defaults v1.8.0 github.com/creasty/defaults v1.8.0
github.com/davecgh/go-spew v1.1.1
github.com/go-playground/validator/v10 v10.23.0 github.com/go-playground/validator/v10 v10.23.0
github.com/go-resty/resty/v2 v2.16.2 github.com/go-resty/resty/v2 v2.16.2
github.com/goccy/go-yaml v1.15.7 github.com/goccy/go-yaml v1.15.7
github.com/jmoiron/sqlx v1.4.0 github.com/google/uuid v1.6.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/jessevdk/go-flags v1.6.1
github.com/vishvananda/netlink v1.3.0
github.com/zeebo/blake3 v0.2.4
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/mod v0.22.0 golang.org/x/mod v0.22.0
golang.org/x/text v0.21.0
r00t2.io/clientinfo v0.0.1 r00t2.io/clientinfo v0.0.1
r00t2.io/goutils v1.7.1
r00t2.io/sysutils v1.12.0 r00t2.io/sysutils v1.12.0
) )


require ( require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect dario.cat/mergo v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/djherbis/times v1.6.0 // indirect github.com/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mileusna/useragent v1.3.5 // indirect github.com/mileusna/useragent v1.3.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/afero v1.4.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/crypto v0.30.0 // indirect golang.org/x/crypto v0.30.0 // indirect
golang.org/x/net v0.32.0 // indirect golang.org/x/net v0.32.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
r00t2.io/goutils v1.7.1 // indirect
) )

78
go.sum
View File

@ -1,15 +1,26 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 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/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGznx5A=
github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 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 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -22,43 +33,90 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= 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 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98=
github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 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/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/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8=
github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
r00t2.io/clientinfo v0.0.1 h1:Nz5NmoRbdJMBSMHmtHn9Txs7cc1EFZc+zoDuRLzFG9U= r00t2.io/clientinfo v0.0.1 h1:Nz5NmoRbdJMBSMHmtHn9Txs7cc1EFZc+zoDuRLzFG9U=

333
runner/funcs.go Normal file
View File

@ -0,0 +1,333 @@
package runner

import (
`bytes`
`os`
`os/exec`
`sync`
`time`

`github.com/chigopher/pathlib`
`github.com/google/uuid`
`r00t2.io/gobroke/conf`
`r00t2.io/gobroke/tunnelbroker`
`r00t2.io/goutils/logging`
`r00t2.io/goutils/multierr`
)

// Run takes a conf.Config, applies checks/updates, and any templating/commands if needed.
func Run(cfg *conf.Config, log logging.Logger) (results []*TunnelResult, changed bool, updated bool, err error) {

var wg sync.WaitGroup
var errChan chan error
var doneChan chan bool
var tunChan chan *TunnelResult
var numJobs int
var tmpTun *TunnelResult
var cmd *exec.Cmd
var cmdId uuid.UUID
var stdout *bytes.Buffer = new(bytes.Buffer)
var stderr *bytes.Buffer = new(bytes.Buffer)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)

if cfg.Tunnels == nil || len(cfg.Tunnels) == 0 {
return
}

log.Debug("runner.Run: Running check/update for %d tunnels.", len(cfg.Tunnels))

if !cfg.SingleTunnel {
numJobs = len(cfg.Tunnels)
errChan = make(chan error, numJobs)
tunChan = make(chan *TunnelResult, numJobs)
doneChan = make(chan bool, 1)

log.Debug("runner.Run: Single-tunnel disabled; running async tunnel checks/updates.")

for _, tun := range cfg.Tunnels {
wg.Add(1)
go runAsync(tun, &wg, tunChan, log, errChan)
}

go func() {
wg.Wait()
close(tunChan)
close(errChan)
doneChan <- true
}()

<-doneChan

for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
if tmpTun = <-tunChan; tmpTun != nil {
results = append(results, tmpTun)
if tmpTun.Changed {
changed = true
}
if tmpTun.Updated {
updated = true
}
}
}
} else {
log.Debug("runner.Run: Single-tunnel enabled; running sequential tunnel checks/updates.")

for _, tun := range cfg.Tunnels {
if tmpTun, err = run(tun, log); err != nil {
mErr.AddError(err)
err = nil
}
if tmpTun == nil {
continue
}
results = append(results, tmpTun)
if tmpTun.Changed {
changed = true
}
if tmpTun.Updated {
updated = true
}
}
}

if !mErr.IsEmpty() {
log.Err("runner.Run: Received error(s) running tunnels:\n%v", mErr.Error())
err = mErr
return
}

if cfg.Cmds != nil && len(cfg.Cmds) > 0 {
log.Debug("runner.Run: Running %d commands.", len(cfg.Cmds))
for _, cmdSpec := range cfg.Cmds {
if cmdSpec == nil {
continue
}
if cmdSpec.OnChanges == nil || *cmdSpec.OnChanges == changed {
if cmd, err = cmdSpec.ToCmd(); err != nil {
return
}
cmdId = uuid.New()
stdout.Reset()
stderr.Reset()
cmd.Stdout = stdout
cmd.Stderr = stderr
log.Debug("runner.Run: Command '%s': %s", cmdId.String(), cmd.String())
if err = cmd.Run(); err != nil {
mErr.AddError(err)
err = nil
}
if stdout.Len() > 0 {
log.Debug("runner.run: Command '%s' STDOUT:\n%s", cmdId.String(), stdout.String())
}
if stderr.Len() > 0 {
log.Err("runner.run: Command '%s' STDERR:\n%s", cmdId.String(), stderr.String())
}
}
}
}

if !mErr.IsEmpty() {
log.Err("runner.Run: Received error(s) running commands:\n%v", mErr.Error())
err = mErr
return
}

log.Debug("runner.Run: Finished check/update successfully.")

return
}

// run actually does the thing. This is used if conf.Config.SingleTunnel is true, and wrapped by runAsync.
func run(t *conf.Tunnel, log logging.Logger) (result *TunnelResult, err error) {

var b []byte
var cmd *exec.Cmd
var destPath *pathlib.Path
var destDir *pathlib.Path
var destExists bool
var dirExists bool
var tplChanged bool
var cmdId uuid.UUID
var stdout *bytes.Buffer = new(bytes.Buffer)
var stderr *bytes.Buffer = new(bytes.Buffer)
var tplBuf *bytes.Buffer = new(bytes.Buffer)

var res TunnelResult = TunnelResult{
Config: t,
TunnelBefore: nil,
TunnelAfter: nil,
Updated: false,
Changed: false,
RunTimestamp: time.Now(),
}

log.Debug("runner.run: Running tunnel ID %d.", t.TunnelID)

if res.TunnelBefore, err = tunnelbroker.GetTunnel(t, t.IsDebug()); err != nil {
log.Err("runner.run: Received error getting upstream tunnel configuration for tunnel %d: %v", t.TunnelID, err)
return
}
if res.Updated, err = res.TunnelBefore.Update(); err != nil {
log.Err("runner.run: Received error checking/updating tunnel configuration for tunnel %d: %v", t.TunnelID, err)
return
}
if res.Updated {
log.Debug("runner.run: Tunnel %d is changed.", t.TunnelID)
if res.TunnelAfter, err = tunnelbroker.GetTunnel(t, t.IsDebug()); err != nil {
log.Err("runner.run: Received error getting upstream tunnel configuration for tunnel %d (post-update): %v", t.TunnelID, err)
return
}
} else {
log.Debug("runner.run: Tunnel %d is not changed.", t.TunnelID)
res.TunnelAfter = res.TunnelBefore
}

if t.TemplateConfigs != nil && len(t.TemplateConfigs) > 0 {
log.Debug("runner.run: Running %d templates for tunnel %d.", len(t.TemplateConfigs), t.TunnelID)
for tplIdx, tplSpec := range t.TemplateConfigs {
if tplSpec == nil {
continue
}
log.Debug("runner.run: Running template %d ('%s') for tunnel %d.", tplIdx, tplSpec.Template, t.TunnelID)
tplBuf.Reset()
b = nil
tplChanged = false
if err = tplSpec.Tpl.Execute(tplBuf, res); err != nil {
return
}
destPath = pathlib.NewPath(tplSpec.Dest)
destDir = destPath.Parent()
if destExists, err = destPath.Exists(); err != nil {
return
}
if dirExists, err = destDir.Exists(); err != nil {
return
}
if destExists {
if b, err = os.ReadFile(tplSpec.Dest); err != nil {
return
}
}
if !destExists || !bytes.Equal(b, tplBuf.Bytes()) {
// Doesn't exist or it's a mismatch.
if !dirExists {
// Parent doesn't exist.
if err = destDir.MkdirAllMode(*tplSpec.Perms.ParentDir.Mode); err != nil {
return
}
}
if err = os.WriteFile(tplSpec.Dest, tplBuf.Bytes(), *tplSpec.Perms.File.Mode); err != nil {
return
}
res.Changed = true
tplChanged = true
}
// This is safe to blindly do, as "no-change" support is cooked in.
if err = tplSpec.Perms.Chown(destPath.String()); err != nil {
return
}
if err = tplSpec.Perms.Chmod(destPath.String(), !destExists); err != nil {
return
}
if err = tplSpec.Perms.Chown(destDir.String()); err != nil {
return
}
if err = tplSpec.Perms.Chmod(destDir.String(), !dirExists); err != nil {
return
}

if tplSpec.Cmds != nil && len(tplSpec.Cmds) > 0 {
log.Debug(
"runner.run: Running %d commands for template %d ('%s') for tunnel %d.",
len(tplSpec.Cmds), tplIdx, tplSpec.Template, t.TunnelID,
)
for cmdIdx, cmdSpec := range tplSpec.Cmds {
if cmdSpec == nil {
continue
}
log.Debug(
"runner.run: Command %d for template %d ('%s') for tunnel %d",
cmdIdx, tplIdx, tplSpec.Template, t.TunnelID,
)
if cmdSpec.OnChanges == nil || *cmdSpec.OnChanges == tplChanged {
if cmd, err = cmdSpec.ToCmd(&res); err != nil {
return
}
cmdId = uuid.New()
stdout.Reset()
stderr.Reset()
cmd.Stdout = stdout
cmd.Stderr = stderr
log.Debug("runner.run: Tunnel %d, Template %d '%s': Command '%s': %s", t.TunnelID, tplIdx, tplSpec.Template, cmdId.String(), cmd.String())
if err = cmd.Run(); err != nil {
return
}
if stdout.Len() > 0 {
log.Debug("runner.run: Command '%s' STDOUT:\n%s", cmdId.String(), stdout.String())
}
if stderr.Len() > 0 {
log.Err("runner.run: Command '%s' STDERR:\n%s", cmdId.String(), stderr.String())
}
}
}
}
}
}

if t.Cmds != nil && len(t.Cmds) > 0 {
log.Debug("runner.run: Running %d commands for tunnel %d.", len(t.Cmds), t.TunnelID)
for _, cmdSpec := range t.Cmds {
if cmdSpec == nil {
continue
}
if cmdSpec.OnChanges == nil || *cmdSpec.OnChanges == res.Changed {
if cmd, err = cmdSpec.ToCmd(&res); err != nil {
return
}
cmdId = uuid.New()
stdout.Reset()
stderr.Reset()
cmd.Stdout = stdout
cmd.Stderr = stderr
log.Debug("runner.run: Tunnel %d: Command '%s': %s", t.TunnelID, cmdId.String(), cmd.String())
if err = cmd.Run(); err != nil {
return
}
if stdout.Len() > 0 {
log.Debug("runner.run: Command '%s' STDOUT:\n%s", cmdId.String(), stdout.String())
}
if stderr.Len() > 0 {
log.Err("runner.run: Command '%s' STDERR:\n%s", cmdId.String(), stderr.String())
}
}
}
}

result = &res

log.Debug("runner.run: Finished tunnel %d successfully.", t.TunnelID)

return
}

// runAsync is intended to be used with goroutines. This is used if conf.Config.SingleTunnel is false.
func runAsync(t *conf.Tunnel, wg *sync.WaitGroup, tunChan chan *TunnelResult, log logging.Logger, errChan chan error) {

var err error
var result *TunnelResult

defer wg.Done()

if result, err = run(t, log); err != nil {
errChan <- err
return
}

tunChan <- result

return
}

View File

@ -1,11 +1,27 @@
package runner package runner


import ( import (
`r00t2.io/gobroke/cachedb` `time`

`r00t2.io/gobroke/conf` `r00t2.io/gobroke/conf`
`r00t2.io/gobroke/tunnelbroker`
) )


type Updater struct { // TunnelResult is returned from a Tunnel.Update, and is also passed to the tunnel's templates/templated commands.
cfg *conf.Config type TunnelResult struct {
cache *cachedb.Cache // Config defines the user-provided configuration.
Config *conf.Tunnel
// TunnelBefore is the tunnelbroker.net tunnel configuration before any updates.
TunnelBefore *tunnelbroker.Tunnel
/*
TunnelAfter is the tunnelbroker.net tunnel configuration after any updates.
If no updates were made, this will point to the exact memory as
TunnelBefore.
*/
TunnelAfter *tunnelbroker.Tunnel
// Updated is true if the tunnel's client IP was updated.
Updated bool
// Changed is true if any of the relevant commands/templates/etc. were run/written.
Changed bool
RunTimestamp time.Time
} }

79
tplCmd/consts.go Normal file
View File

@ -0,0 +1,79 @@
package tplCmd

import (
`context`
`net`
`net/netip`
`text/template`

`go4.org/netipx`
)

var (
// Funcs added externally to CombinedTplFuncMap will override any of the funcs defined here or in sprig.
CombinedTplFuncMap template.FuncMap = make(template.FuncMap)
// TODO: github.com/vishvananda/netlink funcs?
TplFuncs = template.FuncMap{
"GetCtx": context.Background,
// stdlib net funcs; everything missing you can get from a net.Resolver (see GetResolver/TplGetResolver)
"CIDRMask": net.CIDRMask,
"InterfaceAddrs": net.InterfaceAddrs,
"InterfaceByIndex": net.InterfaceByIndex,
"InterfaceByName": net.InterfaceByName,
"IPv4": net.IPv4,
"IPv4Mask": net.IPv4Mask,
"JoinHostPort": net.JoinHostPort,
"ParseIP": net.ParseIP,
"ParseMAC": net.ParseMAC,
"ResolveIPAddr": net.ResolveIPAddr,
"ResolveTCPAddr": net.ResolveTCPAddr,
"ResolveUDPAddr": net.ResolveUDPAddr,
"ResolveUnixAddr": net.ResolveUnixAddr,
"TCPAddrFromAddrPort": net.TCPAddrFromAddrPort,
"UDPAddrFromAddrPort": net.UDPAddrFromAddrPort,
// stdlib net/netip funcs
"AddrFrom16": netip.AddrFrom16,
"AddrFrom4": netip.AddrFrom4,
"AddrFromSlice": netip.AddrFromSlice,
"AddrPortFrom": netip.AddrPortFrom,
"IPv4Unspecified": netip.IPv4Unspecified,
"IPv6LinkLocalAllNodes": netip.IPv6LinkLocalAllNodes,
"IPv6LinkLocalAllRouters": netip.IPv6LinkLocalAllRouters,
"IPv6Loopback": netip.IPv6Loopback,
"IPv6Unspecified": netip.IPv6Unspecified,
"ParseAddr": netip.ParseAddr,
"ParseAddrPort": netip.ParseAddrPort,
"ParsePrefix": netip.ParsePrefix,
"PrefixFrom": netip.PrefixFrom,
// go4.org/netipx
"AddrIPNet": netipx.AddrIPNet,
"ComparePrefix": netipx.ComparePrefix,
"FromStdAddr": netipx.FromStdAddr,
"FromStdIP": netipx.FromStdIP,
"FromStdIPNet": netipx.FromStdIPNet,
"IPRangeFrom": netipx.IPRangeFrom,
"ParseIPRange": netipx.ParseIPRange,
"ParsePrefixOrAddr": netipx.ParsePrefixOrAddr,
"PrefixIPNet": netipx.PrefixIPNet,
"PrefixLastIP": netipx.PrefixLastIP,
"RangeOfPrefix": netipx.RangeOfPrefix,
// Custom-defined/compat wrappers
"GetIPSetBuilder": TplGetIPSetBuilder,
"GetResolver": TplGetResolver,
"SplitHostPortHost": TplSplitHostPortHost, // net.SplitHostPort
"SplitHostPortPort": TplSplitHostPortPort, // net.SplitHostPort
"ToCidrHost": TplToCidrHost, // net.ParseCIDR
"ToCidrNet": TplToCidrNet, // net.ParseCIDR
// Weak coercers and other funcs.
"IsNil": TplIsNil,
"ToBool": TplToBool,
"ToFloat": TplToFloat,
"ToInt": TplToInt,
"ToMap": TplToMap,
"ToString": TplToString,
"ToUint": TplToUint,
// Host information
"GetDefaultIface": GetDefaultIface,
"GetSITIface": GetSITIface,
}
)

20
tplCmd/funcs.go Normal file
View File

@ -0,0 +1,20 @@
package tplCmd

import (
`text/template`

`github.com/Masterminds/sprig/v3`
)

// GetTpl returns a generic text/template.Template with the customization/FuncMap logic applied.
func GetTpl() (tpl *template.Template) {

tpl = template.New("")

tpl.Funcs(sprig.TxtFuncMap()).Funcs(TplFuncs)
if CombinedTplFuncMap != nil && len(CombinedTplFuncMap) > 0 {
tpl.Funcs(CombinedTplFuncMap)
}

return
}

View File

@ -7,7 +7,10 @@ import (
`text/template` `text/template`
) )


// ToCmd returns an (os/)exec.Cmd from a TemplateCmd. t should be a tunnelbroker.FetchedTunnel, generally. /*
ToCmd returns an (os/)exec.Cmd from a TemplateCmd.
t should be a runner.TunnelResult.
*/
func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) { func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) {


var progName string var progName string
@ -16,8 +19,15 @@ func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) {
var args []string var args []string
var buf *bytes.Buffer = new(bytes.Buffer) var buf *bytes.Buffer = new(bytes.Buffer)


if !c.IsTemplate {
cmd, err = c.Cmd.ToCmd()
return
}

buf.Reset() buf.Reset()
if tpl, err = template.New("").Parse(c.Program); err != nil {
tpl = GetTpl()
if _, err = tpl.Parse(c.Program); err != nil {
return return
} }
if err = tpl.Execute(buf, t); err != nil { if err = tpl.Execute(buf, t); err != nil {
@ -29,7 +39,8 @@ func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) {
args = make([]string, len(c.Args)) args = make([]string, len(c.Args))
for idx, arg := range c.Args { for idx, arg := range c.Args {
buf.Reset() buf.Reset()
if tpl, err = template.New("").Parse(arg); err != nil { tpl = GetTpl()
if _, err = tpl.Parse(arg); err != nil {
return return
} }
if err = tpl.Execute(buf, t); err != nil { if err = tpl.Execute(buf, t); err != nil {
@ -44,7 +55,8 @@ func (c *TemplateCmd) ToCmd(t any) (cmd *exec.Cmd, err error) {
envs = make([]string, len(c.EnvVars)) envs = make([]string, len(c.EnvVars))
for idx, env := range c.EnvVars { for idx, env := range c.EnvVars {
buf.Reset() buf.Reset()
if tpl, err = template.New("").Parse(env); err != nil { tpl = GetTpl()
if _, err = tpl.Parse(env); err != nil {
return return
} }
if err = tpl.Execute(buf, t); err != nil { if err = tpl.Execute(buf, t); err != nil {

310
tplCmd/funcs_tpl.go Normal file
View File

@ -0,0 +1,310 @@
package tplCmd

import (
`fmt`
`net`
`strconv`
`strings`

`github.com/vishvananda/netlink`
`go4.org/netipx`
)

/*
This file contains functions strictly for use in templates.
*/

// Host functionality
// TODO: How would I do this on non-Linux?

/*
GetDefaultIface returns the interface name for the default route using netlink.
IPv4 by default, IPv6 if ipv6 is true.
If multiple routes match the default route for the inet family,
the lowest metric route's interface will be returned.
*/
func GetDefaultIface(ipv6 bool) (iface string, err error) {

var inetFamily int
var defNet *net.IPNet
var routes []netlink.Route
var defRt *netlink.Route
var defIface *net.Interface
var curPrio int = -1

// This can even be netlink.FAMILY_ALL, but that's silly.
if !ipv6 {
inetFamily = netlink.FAMILY_V4
defNet = &net.IPNet{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
}
} else {
inetFamily = netlink.FAMILY_V6
defNet = &net.IPNet{
IP: net.ParseIP("::"),
Mask: net.CIDRMask(0, 128),
}
}

if routes, err = netlink.RouteList(nil, inetFamily); err != nil {
return
}

for _, route := range routes {
if !(route.Dst != nil || route.Dst.String() == defNet.String()) {
continue
}
if curPrio == -1 {
curPrio = route.Priority
defRt = &route
} else {
if route.Priority < curPrio {
curPrio = route.Priority
defRt = &route
}
}
}

if defRt != nil {
// There's also defRt.ILinkIndex, which is used for VLANs, tunnels, etc.
if defIface, err = net.InterfaceByIndex(defRt.LinkIndex); err != nil {
return
}
if defIface != nil {
iface = defIface.Name
return
}
}

return
}

func GetSITIface() () {

return
}

// Conversions/assertions/coercions.

/*
TplIsNil returns true if v is nil. This lets you determine if e.g. a map or slice is empty or nil.
It currently only really works for a []interface{}, map[interface{}]interface{}, or map[string]interface{}.
*/
func TplIsNil(v interface{}) (isNil bool) {

switch t := v.(type) {
case []interface{}:
isNil = t == nil
case map[interface{}]interface{}:
isNil = t == nil
case map[string]interface{}:
isNil = t == nil
case nil:
isNil = true
}

return
}

// TplToBool attempts to determine a boolean from v. Note that for numbers, b is true if v is *NOT* 0.
func TplToBool(v interface{}) (b bool, err error) {

switch t := v.(type) {
case bool:
b = t
case string:
switch s := strings.ToLower(t); s {
case "true", "y", "yes", "on", "1":
b = true
}
case []byte:
switch s := strings.ToLower(string(t)); s {
case "true", "y", "yes", "on", "1":
b = true
}
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
b = t != 0
}

return
}

/*
TplToFloat returns a float64 from v.
Strings will be run through strconv.ParseFloat with 64 bitness.
Mind overflows.
*/
func TplToFloat(v interface{}) (i float64, err error) {

switch t := v.(type) {
case uint, uint8, uint16, uint32, uint64:
i = float64(t.(uint64))
case int, int8, int16, int32, int64:
i = float64(t.(int64))
case float64:
i = t
case float32:
i = float64(t)
case string:
if i, err = strconv.ParseFloat(t, 64); err != nil {
return
}
case []byte:
if i, err = strconv.ParseFloat(string(t), 64); err != nil {
return
}
}

return
}

/*
TplToInt returns a signed 64-bit integer from primitives.
Strings will be run through strconv.ParseInt with base 10 and 64 bitness.
Mind overflows.
*/
func TplToInt(v interface{}) (i int64, err error) {

switch t := v.(type) {
case uint, uint8, uint16, uint32, uint64:
i = int64(t.(uint64)) // If an overflow happens anywhere, it's either here or the string/[]bytes.
case int64:
i = t
case int, int8, int16, int32:
i = t.(int64)
case float32, float64:
i = int64(t.(float64))
case string:
if i, err = strconv.ParseInt(t, 10, 64); err != nil {
return
}
case []byte:
if i, err = strconv.ParseInt(string(t), 10, 64); err != nil {
return
}
}

return
}

/*
TplToMap attempts to return a map[string]interface{} from v.
Values can then be used with other TplTo* functions further.
m will be nil if it can't be asserted.
*/
func TplToMap(v interface{}) (m map[string]interface{}, err error) {

switch t := v.(type) {
case map[string]interface{}:
m = t
}

return
}

// TplToString returns a string representation of primitives.
func TplToString(v interface{}) (s string, err error) {

switch t := v.(type) {
case string:
s = t
case []byte:
s = string(t)
default:
s = fmt.Sprintf("%v", v)
}

return
}

/*
TplToUint returns an unsigned 64-bit integer from primitives.
Strings will be run through strconv.ParseUint with base 10 and 64 bitness.
Mind overflows.
*/
func TplToUint(v interface{}) (i uint64, err error) {

switch t := v.(type) {
case uint64:
i = t
case uint, uint8, uint16, uint32:
i = t.(uint64)
case int, int8, int16, int32, int64:
i = uint64(t.(int64))
case float32, float64:
i = uint64(t.(float64))
case string:
if i, err = strconv.ParseUint(t, 10, 64); err != nil {
return
}
case []byte:
if i, err = strconv.ParseUint(string(t), 10, 64); err != nil {
return
}
}

return
}

// Wrappers

// TplGetIPSetBuilder returns a netipx.IPSetBuilder.
func TplGetIPSetBuilder() (ipsb *netipx.IPSetBuilder) {

ipsb = new(netipx.IPSetBuilder)
*ipsb = netipx.IPSetBuilder{}

return
}

// TplGetResolver returns a net.Resolver from the given options.
func TplGetResolver(useGo, strictErr bool) (resolver *net.Resolver) {

resolver = &net.Resolver{
PreferGo: useGo,
StrictErrors: strictErr,
}

return
}

// TplSplitHostPortHost wraps net.SplitHostPort and returns the host.
func TplSplitHostPortHost(s string) (host string, err error) {

if host, _, err = net.SplitHostPort(s); err != nil {
return
}

return
}

// TplSplitHostPortPort wraps net.SplitHostPort and returns the port.
func TplSplitHostPortPort(s string) (port string, err error) {

if _, port, err = net.SplitHostPort(s); err != nil {
return
}

return
}

// TplToCidrHost wraps net.ParseCIDR and returns the host net.IP and any error.
func TplToCidrHost(s string) (host net.IP, err error) {

if host, _, err = net.ParseCIDR(s); err != nil {
return
}

return
}

// TplToCidrNet wraps net.ParseCIDR and returns the network *net.IPNet and any error.
func TplToCidrNet(s string) (netwk *net.IPNet, err error) {

if _, netwk, err = net.ParseCIDR(s); err != nil {
return
}

return
}

View File

@ -53,7 +53,7 @@ func GetTunnel(cfg *conf.Tunnel, debug bool) (tun *Tunnel, err error) {


tun = tuns.Tunnels[0] tun = tuns.Tunnels[0]
tun.client = client tun.client = client
tun.tunCfg = cfg tun.TunCfg = cfg


return return
} }

View File

@ -9,6 +9,16 @@ import (
"r00t2.io/clientinfo/server" "r00t2.io/clientinfo/server"
) )


// Has48 returns true if this Tunnel has a /48 assigned.
func (t *Tunnel) Has48() (has48 bool) {

if t.Routed48 != nil {
has48 = true
}

return
}

/* /*
Update checks the current (or explicit) client IPv4 address, compares it against the Tunnel's configuration, Update checks the current (or explicit) client IPv4 address, compares it against the Tunnel's configuration,
and updates itself on change. and updates itself on change.
@ -20,10 +30,9 @@ func (t *Tunnel) Update() (updated bool, err error) {
var req *resty.Request var req *resty.Request
var targetIp net.IP var targetIp net.IP
var respStrs []string var respStrs []string
var newTun *Tunnel = new(Tunnel)


if t.tunCfg.ExplicitAddr != nil { if t.TunCfg.ExplicitAddr != nil {
targetIp = *t.tunCfg.ExplicitAddr targetIp = *t.TunCfg.ExplicitAddr
} else { } else {
// Fetch the current client IP. // Fetch the current client IP.
// Teeechnically we don't need to do this, as it by default uses client IP, but we wanna be as considerate as we can. // Teeechnically we don't need to do this, as it by default uses client IP, but we wanna be as considerate as we can.
@ -45,8 +54,8 @@ func (t *Tunnel) Update() (updated bool, err error) {
if !t.ClientIPv4.Equal(targetIp) { if !t.ClientIPv4.Equal(targetIp) {
// It's different, so update. // It's different, so update.
req = t.client.R() req = t.client.R()
req.SetBasicAuth(*t.tunCfg.Username, t.tunCfg.UpdateKey) req.SetBasicAuth(*t.TunCfg.Username, t.TunCfg.UpdateKey)
req.SetQueryParam(updateTidParam, fmt.Sprintf("%d", t.tunCfg.TunnelID)) req.SetQueryParam(updateTidParam, fmt.Sprintf("%d", t.TunCfg.TunnelID))
req.SetQueryParam(updateIpParam, targetIp.To4().String()) req.SetQueryParam(updateIpParam, targetIp.To4().String())


if resp, err = req.Get(updateBaseUrl); err != nil { if resp, err = req.Get(updateBaseUrl); err != nil {
@ -59,20 +68,17 @@ func (t *Tunnel) Update() (updated bool, err error) {
respStrs = strings.Fields(resp.String()) respStrs = strings.Fields(resp.String())
if respStrs == nil || len(respStrs) == 0 { if respStrs == nil || len(respStrs) == 0 {
// I... don't know what would result in this, but let's assume it succeeded. // I... don't know what would result in this, but let's assume it succeeded.
if newTun, err = GetTunnel(t.tunCfg, t.client.Debug); err != nil {
return
}
updated = true updated = true
*t = *newTun

return return
} }
switch len(respStrs) { switch len(respStrs) {
case 1: case 1:
switch respStrs[0] { if respStrs[0] == "abuse" {
case "abuse":
err = ErrHERateLimit err = ErrHERateLimit
return return
}
case 2:
switch respStrs[0] {
case "nochg": case "nochg":
// No update; existing value is the same // No update; existing value is the same
return return
@ -89,9 +95,7 @@ func (t *Tunnel) Update() (updated bool, err error) {
return return
} }
} }
case 2:
} }

} }


return return

View File

@ -17,33 +17,55 @@ import (
*/ */
type TunPrefix netip.Prefix type TunPrefix netip.Prefix


// TunnelList is what's returned from the tunnelbroker.net API, regardless if a specific tunnel ID is specified or not.
type TunnelList struct { type TunnelList struct {
XMLName xml.Name `json:"-" xml:"tunnels" yaml:"-"` XMLName xml.Name `json:"-" xml:"tunnels" yaml:"-"`
// Tunnels should only contain a single Tunnel if a (valid) tunnel ID was specified.
Tunnels []*Tunnel `json:"tunnels" xml:"tunnel" yaml:"Tunnels"` Tunnels []*Tunnel `json:"tunnels" xml:"tunnel" yaml:"Tunnels"`
} }


// Tunnel is a single tunnel configuration as returned from the tunnelbroker.net API.
type Tunnel struct { type Tunnel struct {
XMLName xml.Name `json:"-" xml:"tunnel" yaml:"-"` XMLName xml.Name `json:"-" xml:"tunnel" yaml:"-"`
// ID should correspond with a conf.Tunnel.ID.
ID uint `json:"id" xml:"id,attr" yaml:"ID" db:"tun_id"` ID uint `json:"id" xml:"id,attr" yaml:"ID" db:"tun_id"`
// Description is generally thought of more as a "friendly name" for the tunnel.
Description string `json:"desc" xml:"description" yaml:"Description" db:"desc"` Description string `json:"desc" xml:"description" yaml:"Description" db:"desc"`
// ServerIPv4 is the "tunnel server"; the SIT client should use this as the server.
ServerIPv4 net.IP `json:"tgt_v4" xml:"serverv4" yaml:"IPv4 Tunnel Target" db:"server_v4"` ServerIPv4 net.IP `json:"tgt_v4" xml:"serverv4" yaml:"IPv4 Tunnel Target" db:"server_v4"`
// ClientIPv4 is the *currently configured* "authorized client IP"; this should be the WAN-routable address of the client end of the SIT.
ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"` ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"`
// ServerIPv6 is the gateway end that your SIT address (ClientIPv6) "peers" with.
ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"` ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"`
// ClientIPv6 is the address that should be assigned on your server's SIT interface.
ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"` ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"`
// Routed64 is the IPv6 prefix that gets routed to ClientIPv6. All tunnels have this.
Routed64 TunPrefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"` Routed64 TunPrefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"`
// Routed48 may or may not be present, and only available after a certain level of HE certification has been completed and it has been allocated in the web UI.
Routed48 *TunPrefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"` Routed48 *TunPrefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"`
// RDNS1 is the first RDNS you have specified for the tunnel, if any.
RDNS1 *string `json:"rdns_1,omitempty" xml:"rdns1,omitempty" yaml:"RDNS #1,omitempty" db:"rdns_1"` RDNS1 *string `json:"rdns_1,omitempty" xml:"rdns1,omitempty" yaml:"RDNS #1,omitempty" db:"rdns_1"`
// RDNS2 is the second RDNS you have specified for the tunnel, if any.
RDNS2 *string `json:"rdns_2,omitempty" xml:"rdns2,omitempty" yaml:"RDNS #2,omitempty" db:"rdns_2"` RDNS2 *string `json:"rdns_2,omitempty" xml:"rdns2,omitempty" yaml:"RDNS #2,omitempty" db:"rdns_2"`
// RDNS3 is the third RDNS you have specified for the tunnel, if any.
RDNS3 *string `json:"rdns_3,omitempty" xml:"rdns3,omitempty" yaml:"RDNS #3,omitempty" db:"rdns_3"` RDNS3 *string `json:"rdns_3,omitempty" xml:"rdns3,omitempty" yaml:"RDNS #3,omitempty" db:"rdns_3"`
// RDNS4 is the fourth RDNS you have specified for the tunnel, if any.
RDNS4 *string `json:"rdns_4,omitempty" xml:"rdns4,omitempty" yaml:"RDNS #4,omitempty" db:"rdns_4"` RDNS4 *string `json:"rdns_4,omitempty" xml:"rdns4,omitempty" yaml:"RDNS #4,omitempty" db:"rdns_4"`
// RDNS5 is the fifth RDNS you have specified for the tunnel, if any.
RDNS5 *string `json:"rdns_5,omitempty" xml:"rdns5,omitempty" yaml:"RDNS #5,omitempty" db:"rdns_5"` RDNS5 *string `json:"rdns_5,omitempty" xml:"rdns5,omitempty" yaml:"RDNS #5,omitempty" db:"rdns_5"`
tunCfg *conf.Tunnel // TunCfg is the tunnel as defined in the local configuration file associated with this Tunnel.
TunCfg *conf.Tunnel `json:"-" xml:"-" yaml:"-"`
client *resty.Client client *resty.Client
} }


// HTTPError is a handler for non-success HTTP(S) requests.
type HTTPError struct { type HTTPError struct {
// Code is the status code as reported by the server.
Code int `json:"code" xml:"code,attr" yaml:"Status Code"` Code int `json:"code" xml:"code,attr" yaml:"Status Code"`
// CodeStr is a more human-friendly string. It includes Code.
CodeStr string `json:"code_str" xml:"code_str,attr" yaml:"Status Code (Detailed)"` CodeStr string `json:"code_str" xml:"code_str,attr" yaml:"Status Code (Detailed)"`
// Message is any message sent from the server in the response's body, if any.
Message *string `json:"message,omitempty" xml:",chardata" yaml:"Error Message,omitempty"` Message *string `json:"message,omitempty" xml:",chardata" yaml:"Error Message,omitempty"`
// Resp is the actual response received.
Resp *resty.Response `json:"-" xml:"-" yaml:"-"` Resp *resty.Response `json:"-" xml:"-" yaml:"-"`
} }