disabling cache; it's not really necessary.

This commit is contained in:
brent saner 2024-12-20 01:29:56 -05:00
parent c0af14d890
commit 3b4d712722
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
24 changed files with 941 additions and 112 deletions

23
README.adoc Normal file
View File

@ -0,0 +1,23 @@
= GoBroke
r00t^2 <brent.saner@gmail.com>
Last rendered {localdatetime}
:doctype: book
:docinfo: shared
:data-uri:
:imagesdir: images
:sectlinks:
:sectnums:
:sectnumlevels: 7
:toc: preamble
:toc2: left
:idprefix:
:toclevels: 7
//:toclevels: 4
:source-highlighter: rouge
:docinfo: shared

[id="wat"]
== What is It?
**GoBroke** is a client/management program for Hurricane Electric's https://tunnelbroker.net/[`tunnelbroker.net`^] service.

NOTE: GoBroke has absolutely no affiliation with Hurricane Electric, tunnelbroker.net, or any other parties. I will not provide support for their services, and they will not provide support for this software.

View File

@ -1,3 +0,0 @@
# GoBroke

A Golang tunnelbroker.net client/manager.

View File

@ -1,6 +1,8 @@
PRAGMA foreign_keys= OFF;
PRAGMA journal_mode = WAL;
BEGIN TRANSACTION;
CREATE TABLE tunnels (
CREATE TABLE tunnels
(
tun_id INTEGER NOT NULL PRIMARY KEY,
cksum_crc32 INTEGER NOT NULL,
"desc" TEXT,
@ -15,17 +17,27 @@ CREATE TABLE tunnels (
rdns_3 TEXT,
rdns_4 TEXT,
rdns_5 TEXT,
created INTEGER NOT NULL,
checked INTEGER NOT NULL,
updated INTEGER
created TIMESTAMP NOT NULL,
checked TIMESTAMP NOT NULL,
updated TIMESTAMP
);
CREATE TABLE client_ips (
CREATE TABLE client_ips
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
tun_id INTEGER NOT NULL,
client_ip INTEGER NOT NULL,
when_set INTEGER NOT NULL, when_fetched INTEGER,
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);
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

@ -8,8 +8,3 @@ var (
//go:embed "_static/cache.schema.sql"
schemaBytes []byte
)

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

View File

@ -4,6 +4,9 @@ import (
`os`
`path/filepath`

`github.com/jmoiron/sqlx`
_ `github.com/mattn/go-sqlite3`
`r00t2.io/gobroke/conf`
`r00t2.io/sysutils/paths`
)

@ -12,24 +15,59 @@ import (

It will be created if it doesn't exist for persistent caches.
*/
func NewCache(db string) (c *Cache, err error) {
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), 0700); err != nil {
if err = os.MkdirAll(filepath.Dir(db), *perms.ParentDir.Mode); err != nil {
return
}
if err = os.WriteFile()
if err = os.WriteFile(db, nil, *perms.File.Mode); err != nil {
return
}
}
// TODO
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
}

13
cachedb/funcs_cache.go Normal file
View File

@ -0,0 +1,13 @@
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,27 +1,53 @@
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
}

type TunnelDB struct {
/*
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"`
}

type ClientIpDB struct {
ID uint64 `db:"id"`
TunID uint64 `db:"tun_id"`
*tunnelbroker.FetchedIP
/*
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"`
}

View File

@ -1,3 +1,10 @@
# 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 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,
@ -50,16 +57,17 @@ CacheDbPath = '/var/cache/gobroke.db'
# and the parent directory (see below).
[CacheDbPerms.File]
# The User is optional.
# If unspecified, the default behavir mentioned above is performed.
# If specified as an empty string, the runtime EUID is enforced.
# Otherwise it may be a username or a UID (checked in that order).
# 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 logic except
# 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), but may be represented in multiple ways.
# unsigned integer 0-4095), but may be represented in multiple ways.
# e.g.:
# Mode = 0o0600
# Mode = 0o600
@ -75,12 +83,15 @@ CacheDbPath = '/var/cache/gobroke.db'
# you can use the calculator here:
# https://rubendougall.co.uk/projects/permissions-calculator/
# (source: https://github.com/Ruben9922/permissions-calculator )
# (Supports "special" bits)
# (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.

View File

@ -0,0 +1,57 @@
DefaultUsername = "default_user"
SingleTunnel = true
CacheDbPath = '/var/cache/gobroke.db'
[CacheDbPerms]
[CacheDbPerms.File]
User = ""
Group = ""
Mode = 0o0600
[CacheDbPerms.Dir]
User = ""
Group = ""
Mode = 0o0700
[[Tunnel]]
TunnelID = 123
ExplicitClientIP = '203.0.113.1'
MTU = 1450
Username = "specific_user"
UpdateKey = "abcdef"
[[Tunnel.ConfigTemplate]]
Template = "/etc/gobroke/tpl/dnsmasq/ra_dhcpv6.conf.tpl"
Destination = "/etc/dnsmasq.d/ra_dhcpv6.conf"
[[Tunnel.ConfigTemplate.Permissions]]
[[Tunnel.ConfigTemplate.Permissions.File]]
User = ""
Group = ""
Mode = 0o0600
[[Tunnel.ConfigTemplate.Permissions.Dir]]
User = ""
Group = ""
Mode = 0o0700
[[Tunnel.ConfigTemplate.Command]]
ProgramPath = '/usr/local/bin/somecmd'
Args = [
'-f', 'foo',
]
IsolatedEnv = false
EnvVars = [
'SOMEENV=SOMEVAL',
]
OnChange = true
IsTemplate = false
[[Tunnel.ConfigTemplate]]
Template = "/etc/gobroke/tpl/stat.tpl"
Destination = "/tmp/gobroke.dump"
[[Tunnel.Command]]
ProgramPath = 'systemctl'
Args = [
'restart',
'someservice',
]
OnChange = true
[[Tunnel]]
TunnelID = 456
Username = "specific_user"
UpdateKey = "defghi"
[[Command]]
ProgramPath = "/usr/local/bin/alltunpsrogram"

View File

@ -9,6 +9,7 @@ import (
"r00t2.io/sysutils/paths"
)

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

var b []byte
@ -17,11 +18,16 @@ func NewConfig(path string) (cfg *Config, err error) {
return
}

cfg, err = NewConfigFromBytes(b)
if cfg, err = NewConfigFromBytes(b); err != nil {
return
}
cfg.confPath = new(string)
*cfg.confPath = path

return
}

// 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) {

if err = json.Unmarshal(b, &cfg); err != nil {
@ -43,6 +49,12 @@ func NewConfigFromBytes(b []byte) (cfg *Config, err error) {
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 {
@ -67,6 +79,11 @@ func NewConfigFromBytes(b []byte) (cfg *Config, err error) {
if err = paths.RealPath(&tpl.Dest); err != nil {
return
}
if tpl.Perms != nil {
if err = tpl.Perms.SetMissing(); err != nil {
return
}
}
}
}
}

106
conf/funcs_perms.go Normal file
View File

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

import (
`io/fs`
`os`
`os/user`
`strconv`

`r00t2.io/sysutils/paths`
)

// Chmod enforces perms for a file or directory.
func (p *Perms) Chmod(path string, isNew bool) (err error) {

var fi fs.FileInfo

if err = paths.RealPath(&path); err != nil {
return
}
if fi, err = os.Stat(path); err != nil {
return
}

// If we add additional spec types (e.g. sockets, etc.), make this a switch.
if fi.IsDir() {
if p.ParentDir != nil {
if err = p.ParentDir.chmod(path, isNew); err != nil {
return
}
}
} else {
if p.File != nil {
if err = p.File.chmod(path, isNew); err != nil {
return
}
}
}

return
}

// Chown enforces owner/group for a file or directory.
func (p *Perms) Chown(path string) (err error) {

var fi fs.FileInfo

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

// If we add additional spec types (e.g. sockets, etc.), make this a switch.
if fi.IsDir() {
if p.ParentDir != nil {
if err = p.ParentDir.chown(path); err != nil {
return
}
}
} else {
if p.File != nil {
if err = p.File.chown(path); err != nil {
return
}
}
}

return
}

// SetMissing populates any missing fields.
func (p *Perms) SetMissing() (err error) {

if p.curUser == nil {
if p.curUser, err = user.Current(); err != nil {
return
}
if p.curUid, err = strconv.Atoi(p.curUser.Uid); err != nil {
return
}
}
if p.curGroup == nil {
if p.curGroup, err = user.LookupGroupId(p.curGroup.Gid); err != nil {
return
}
if p.curGid, err = strconv.Atoi(p.curGroup.Gid); err != nil {
return
}
}

if p.File == nil {
p.File = new(PermSpec)
}
p.File.parent = p
if p.ParentDir == nil {
p.ParentDir = new(PermSpec)
}
p.ParentDir.parent = p

if err = p.File.setMissing(false); err != nil {
return
}
if err = p.ParentDir.setMissing(true); err != nil {
return
}

return
}

123
conf/funcs_permspec.go Normal file
View File

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

import (
`errors`
`io/fs`
`os`
`os/user`
`strconv`
)

// chmod applies the PermSpec.Mode to path.
func (p *PermSpec) chmod(path string, isNew bool) (err error) {

if p.Mode == nil || (!isNew && p.explicitMode == false) {
return
}

if err = os.Chmod(path, *p.Mode); err != nil {
return
}

return
}

// chown applies the Permspec.User and PermSpec.Group to path.
func (p *PermSpec) chown(path string) (err error) {

/*
ORIGINALLY, I thought I'd have to fetch the original UID/GID from fs.FileInfo.Sys().
Linux uses https://pkg.go.dev/syscall?GOOS=linux#Stat_t
macOS uses a https://pkg.go.dev/syscall?GOOS=darwin#Stat_t
Windows uses a https://pkg.go.dev/syscall?GOOS=windows#Win32FileAttributeData which is completely useless.
(And AIX, Plan9, JS don't have a Stat_t.)
But per os.Chown, a -1 means "do not change", which is what we want.
*/

if (p.realUid == -1) || (p.realGid == -1) {
// This evaluates as a no-op.
return
}

if err = os.Chown(path, p.realUid, p.realGid); err != nil {
return
}

return
}

// setMissing populates missing information from a PermSpec. It should only be invoked by the parent Perms.SetMissing.
func (p *PermSpec) setMissing(isDir bool) (err error) {

var tmpUser *user.User
var tmpGroup *user.Group
var unameUnknown user.UnknownUserError
// var uidUnknown user.UnknownUserIdError
var gnameUnknown user.UnknownGroupError
// var gidUnknown user.UnknownGroupIdError

if p.idsSet {
return
}

// MODE
if p.Mode == nil {
p.Mode = new(fs.FileMode)
if isDir {
*p.Mode = fs.FileMode(0o0700)
} else {
*p.Mode = fs.FileMode(0o0600)
}
} else {
p.explicitMode = true
}

// OWNER/GROUP
// If nil, no change from current on-disk.
switch p.User {
case "":
p.realUid = p.parent.curUid
case "-1":
p.realUid = -1
default:
// Lookup, try username first then uid.
if tmpUser, err = user.Lookup(p.User); err != nil {
if errors.As(err, &unameUnknown) {
err = nil
if tmpUser, err = user.LookupId(p.User); err != nil {
return
}
} else {
return
}
}
if p.realUid, err = strconv.Atoi(tmpUser.Uid); err != nil {
return
}
}
switch p.Group {
case "":
p.realGid = p.parent.curGid
case "-1":
p.realGid = -1
default:
// Lookup, try groupname first then gid.
if tmpGroup, err = user.LookupGroup(p.Group); err != nil {
if errors.As(err, &gnameUnknown) {
err = nil
if tmpGroup, err = user.LookupGroupId(p.Group); err != nil {
return
}
} else {
return
}
}
if p.realGid, err = strconv.Atoi(tmpGroup.Gid); err != nil {
return
}
}

p.idsSet = true

return
}

View File

@ -2,8 +2,9 @@ package conf

import (
"encoding/xml"
`io/fs`
`net`
`os`
`os/user`

`r00t2.io/gobroke/tplCmd`
)
@ -27,12 +28,13 @@ type Config struct {
// CacheDbPerms specifies the optional permissions for the file and parent directory for CacheDB; only used if persistent cache.
CacheDbPerms *Perms `json:"cache_perms,omitempty" toml:"CacheDbPerms,omitempty" xml:"cachePerms,omitempty" yaml:"Cache Database Permissions,omitempty"`
// Tunnels contains one or more tunnel configurations.
Tunnels []*Tunnel `json:"tunnels" toml:"Tunnel" xml:"tunnels>tunnel" yaml:"Tunnels" validate:"required"`
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.
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"`
Cmds []tplCmd.Cmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"`
confPath *string
}

// Tunnel represents a single tunnel configuration from tunnelbroker.net.
@ -49,7 +51,7 @@ type Tunnel struct {
ExplicitAddr, if provided, will be used as the tunnelbroker.FetchedTunnel.CurrentIPv4.
If not provided, this will be fetched dynamically from an external source.
*/
ExplicitAddr *net.IP `json:"addr,omitempty" toml:"ExplicitClientIP,omitempty" xml:"addr,attr,omitempty" yaml:"Explicit Client IP Address,omitempty"`
ExplicitAddr *net.IP `json:"addr,omitempty" toml:"ExplicitClientIP,omitempty" xml:"addr,attr,omitempty" yaml:"Explicit Client IP Address,omitempty" validate:"omitempty,ipv4"`
/*
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
@ -69,13 +71,13 @@ type Tunnel struct {
*/
UpdateKey string `json:"update_key" toml:"UpdateKey" xml:"key,attr" yaml:"Update Key" validate:"required"`
// TemplateConfgs is optional. It holds templates that will be executed in order given. See ConfigTemplate.
TemplateConfigs []ConfigTemplate `json:"cfg_tpls" toml:"ConfigTemplate" xml:"config>tpl" yaml:"Configuration File Templates"`
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).
Each command will also have tunnelbroker.FetchedTunnel templated to it like TemplateConfigs/ConfigTemplate.Commands,
so they may be templated as necessary.
*/
Cmds []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"commands>cmd,omitempty" yaml:"Commands,omitempty"`
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
}
@ -101,7 +103,7 @@ type ConfigTemplate struct {
// Perms allows specifying permissions/ownerships, if the curent user has the capability to do so.
Perms *Perms `json:"perms,omitempty" toml:"Permissions,omitempty" xml:"perms,omitempty" yaml:"Permissions and Ownership,omitempty"`
// Commands specifiies commands to run after this ConfigTemplate run.
Commands []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"cmds>cmd,omitempty" yaml:"Commands,omitempty"`
Commands []tplCmd.TemplateCmd `json:"cmds,omitempty" toml:"Command,omitempty" xml:"cmds>cmd,omitempty" yaml:"Commands,omitempty" validate:"omitempty,dive"`
}

type Perms struct {
@ -109,6 +111,10 @@ type Perms struct {
File *PermSpec `json:"file,omitempty" toml:"File,omitempty" xml:"file,omitempty" yaml:"File,omitempty"`
// ParentDir specifies the desired permissions/ownership of the parent ("dirname") of File.
ParentDir *PermSpec `json:"dir,omitempty" toml:"Dir,omitempty" xml:"dir,omitempty" yaml:"Directory,omitempty"`
curUser *user.User
curGroup *user.Group
curUid int
curGid int
}

type PermSpec struct {
@ -117,13 +123,18 @@ type PermSpec struct {
If specified as an empty string, the current/runtime UID will be used.
If unspecified, UID will not be enforced.
*/
User *string `json:"user,omitempty" toml:"User,omitempty" xml:"user,attr,omitempty" yaml:"User,omitempty"`
User string `json:"user,omitempty" toml:"User,omitempty" xml:"user,attr,omitempty" yaml:"User,omitempty"`
/*
Group is the groupname or GID (tried in that order) to chown.
If specified as an empty string, the current/runtime GID will be used.
If unspecified, GID will not be enforced.
*/
Group *string `json:"group,omitempty" toml:"Group,omitempty" xml:"group,attr,omitempty" yaml:"Group,omitempty"`
Group string `json:"group,omitempty" toml:"Group,omitempty" xml:"group,attr,omitempty" yaml:"Group,omitempty"`
// Mode is the permission mode bitset. If unspecified, mode will not be enforced.
Mode *os.FileMode `json:"mode,omitempty" toml:"Mode,omitempty" xml:"mode,attr,omitempty" yaml:"Mode,omitempty"`
Mode *fs.FileMode `json:"mode,omitempty" toml:"Mode,omitempty" xml:"mode,attr,omitempty" yaml:"Mode,omitempty" validate:"omitempty,ge=0,le=4095"`
explicitMode bool
realUid int
realGid int
idsSet bool
parent *Perms
}

19
go.mod
View File

@ -9,19 +9,26 @@ require (
github.com/go-resty/resty/v2 v2.16.2
github.com/goccy/go-yaml v1.15.7
github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.22
golang.org/x/mod v0.22.0
r00t2.io/clientinfo v0.0.1
r00t2.io/sysutils v1.12.0
)

require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.16.0 // indirect
github.com/mileusna/useragent v1.3.5 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/sync v0.10.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
)

33
go.sum
View File

@ -2,6 +2,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
@ -9,8 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -26,6 +27,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98=
github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@ -34,26 +37,32 @@ 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/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
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/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
r00t2.io/clientinfo v0.0.1 h1:Nz5NmoRbdJMBSMHmtHn9Txs7cc1EFZc+zoDuRLzFG9U=
r00t2.io/clientinfo v0.0.1/go.mod h1:CRup6hAQ6EaOrzyCYUWR27BACJCEUf7YFKVYoE9ivaY=
r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc=
r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=

11
runner/types.go Normal file
View File

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

import (
`r00t2.io/gobroke/cachedb`
`r00t2.io/gobroke/conf`
)

type Updater struct {
cfg *conf.Config
cache *cachedb.Cache
}

View File

@ -1,7 +1,7 @@
package tunnelbroker

const (
wanIpUrl string = "https://c4.r00t2.io/ip"
wanIpUrl string = "https://c4.r00t2.io/"
// https://forums.he.net/index.php?topic=3153.0
// If no TID is provided, all tunnels are returned.
/*
@ -19,5 +19,6 @@ const (
If left off, it defaults to client IP (as seen by the webserver).
*/
updateIpParam string = "myip"
noTunBody string = "No tunnels found"
// respons messages
tunRespNoTuns string = "No tunnels found"
)

12
tunnelbroker/errs.go Normal file
View File

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

import (
`errors`
)

var (
ErrBadPrefixValue error = errors.New("cannot reliably determine a TunPrefix or netip.Prefix from value")
ErrHERateLimit error = errors.New("the Hurricane Electric soft rate limit has been hit; please lower your frequency or you will get a 429")
ErrHENoTuns error = errors.New("no tunnel configuration found for the specified tunnel ID")
ErrHEInvalid error = errors.New("the new client IP address is either not allowed or cannot be pinged")
)

View File

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

// NewClient reuturns a Client.
func NewClient() (c *Client, err error) {
import (
`fmt`
`runtime`
`strings`

// TODO
`github.com/go-resty/resty/v2`
`golang.org/x/text/cases`
`golang.org/x/text/language`
`r00t2.io/gobroke/conf`
`r00t2.io/gobroke/version`
)

// GetTunnel returns a tunnel configuration from tunnelbroker.net.
func GetTunnel(cfg *conf.Tunnel, debug bool) (tun *Tunnel, err error) {

var tuns TunnelList
var resp *resty.Response
var req *resty.Request
var client *resty.Client

// Set up the client. Namely the UA.
client = resty.New()
client.SetDebug(debug)
client.SetHeader(
"User-Agent",
fmt.Sprintf(
"GoBroke/%s go-resty/%s Go/%s "+
"(%s %s) "+
"(https://pkg.go.dev/r00t2.io/gobroke)",
version.Ver.Short(), resty.Version, runtime.Version(),
cases.Title(language.English).String(runtime.GOOS), runtime.GOARCH,
),
)

req = client.R()
req.SetResult(&tuns)
req.SetBasicAuth(*cfg.Username, cfg.UpdateKey)
req.SetQueryParam(infoTidParam, fmt.Sprintf("%d", cfg.TunnelID))

if resp, err = req.Get(infoBaseUrl); err != nil {
return
}
if !resp.IsSuccess() {
err = respToErr(resp)
return
}
if strings.HasPrefix(resp.String(), tunRespNoTuns) {
err = ErrHENoTuns
return
}

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

return
}

// respToErr returns an HTTPError from a resty.Response. err will be nill if the response was a success.
func respToErr(resp *resty.Response) (err *HTTPError) {

if resp.IsSuccess() {
return
}

err = &HTTPError{
Code: resp.StatusCode(),
CodeStr: resp.Status(),
Message: nil,
Resp: resp,
}

if resp.String() != "" {
err.Message = new(string)
*err.Message = resp.String()
}

return
}

View File

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

import (
`fmt`
)

// Error conforms an HTTPError to an error.
func (h *HTTPError) Error() (errMsg string) {

errMsg = h.CodeStr
if h.Message != nil {
errMsg += fmt.Sprintf(":\n%s", *h.Message)
}

return
}

View File

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

import (
`encoding/json`
`fmt`
`os`
`strconv`
"testing"

`r00t2.io/gobroke/conf`
`r00t2.io/gobroke/tplCmd`
`r00t2.io/gobroke/version`
`r00t2.io/sysutils/envs`
)

func TestUpdate(t *testing.T) {
var err error
var s string
var b []byte
var tun *Tunnel
var u64 uint64
var updated bool
var tuncfg *conf.Tunnel = &conf.Tunnel{
TunnelID: 0,
ExplicitAddr: nil,
MTU: 1480,
Username: nil,
UpdateKey: "",
TemplateConfigs: nil,
Cmds: []tplCmd.TemplateCmd{
tplCmd.TemplateCmd{
Cmd: &tplCmd.Cmd{
Program: "echo",
Args: []string{
"updated {{ .TunnelID }}",
},
IsolateEnv: false,
EnvVars: nil,
OnChanges: nil,
},
IsTemplate: false,
},
},
}

if version.Ver, err = version.Version(); err != nil {
t.Fatal(err)
}

if !envs.HasEnv("GOBROKE_TUNID") {
t.Fatal("GOBROKE_TUNID not set")
} else {
s = os.Getenv("GOBROKE_TUNID")
if u64, err = strconv.ParseUint(s, 10, 64); err != nil {
t.Fatal(err)
}
tuncfg.TunnelID = uint(u64)
}
if !envs.HasEnv("GOBROKE_USERNAME") {
t.Fatal("GOBROKE_USERNAME not set")
} else {
tuncfg.Username = new(string)
*tuncfg.Username = os.Getenv("GOBROKE_USERNAME")
}
if !envs.HasEnv("GOBROKE_KEY") {
t.Fatal("GOBROKE_KEY not set")
} else {
tuncfg.UpdateKey = os.Getenv("GOBROKE_KEY")
}

if tun, err = GetTunnel(tuncfg, true); err != nil {
t.Fatal(err)
}

if b, err = json.MarshalIndent(tun, "", " "); err != nil {
t.Fatal(err)
}
fmt.Printf("BEFORE UPDATE:\n%s\n", string(b))
if updated, err = tun.Update(); err != nil {
t.Fatal(err)
}
fmt.Printf("Updated:\t%v\n", updated)
if b, err = json.MarshalIndent(tun, "", " "); err != nil {
t.Fatal(err)
}
// spew.Dump(tun)
fmt.Printf("AFTER UPDATE:\n%s\n", string(b))
}

View File

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

import (
`fmt`
`net`
`strings`

`github.com/go-resty/resty/v2`
"r00t2.io/clientinfo/server"
)

/*
Update checks the current (or explicit) client IPv4 address, compares it against the Tunnel's configuration,
and updates itself on change.
*/
func (t *Tunnel) Update() (updated bool, err error) {

var myInfo *server.R00tInfo
var resp *resty.Response
var req *resty.Request
var targetIp net.IP
var respStrs []string
var newTun *Tunnel = new(Tunnel)

if t.tunCfg.ExplicitAddr != nil {
targetIp = *t.tunCfg.ExplicitAddr
} else {
// 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.
req = t.client.R()
// Force the response to JSON; because we pass "Linux" in the UA, it thinks it's graphical...
req.SetHeader("Accept", "application/json")
req.SetResult(&myInfo)

if resp, err = req.Get(wanIpUrl); err != nil {
return
}
if !resp.IsSuccess() {
err = respToErr(resp)
return
}
targetIp = myInfo.IP
}

if !t.ClientIPv4.Equal(targetIp) {
// It's different, so update.
req = t.client.R()
req.SetBasicAuth(*t.tunCfg.Username, t.tunCfg.UpdateKey)
req.SetQueryParam(updateTidParam, fmt.Sprintf("%d", t.tunCfg.TunnelID))
req.SetQueryParam(updateIpParam, targetIp.To4().String())

if resp, err = req.Get(updateBaseUrl); err != nil {
return
}
if !resp.IsSuccess() {
err = respToErr(resp)
return
}
respStrs = strings.Fields(resp.String())
if respStrs == nil || len(respStrs) == 0 {
// 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
*t = *newTun

return
}
switch len(respStrs) {
case 1:
switch respStrs[0] {
case "abuse":
err = ErrHERateLimit
return
case "nochg":
// No update; existing value is the same
return
case "good":
switch respStrs[1] {
case "127.0.0.1":
// If the second returned word is "127.0.0.1", it's a "soft fail".
// This tends to happen if the specified address is in RFC 1918,
// or RFC 5737, or 66.220.2.74 can't ping the address, etc.
err = ErrHEInvalid
return
case targetIp.To4().String():
updated = true
return
}
}
case 2:
}

}

return
}

View File

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

import (
`database/sql/driver`
`net/netip`
)

// MarshalText returns a text representation (as bytes) of a TunPrefix.
func (t *TunPrefix) MarshalText() (b []byte, err error) {

if t == nil {
return
}

b = []byte(t.ToPrefix().String())

return
}

// Scan conforms a TunPrefix to a sql.Scanner. It populates t with val.
func (t *TunPrefix) Scan(val interface{}) (err error) {

var pfx netip.Prefix
var s string

if val == nil {
return
}

switch v := val.(type) {
case string:
s = v
case []byte:
s = string(v)
default:
err = ErrBadPrefixValue
return
}

if pfx, err = netip.ParsePrefix(s); err != nil {
return
}

*t = TunPrefix(pfx)

return
}

// ToPrefix returns a netip.Prefix from a TunPrefix.
func (t *TunPrefix) ToPrefix() (pfx *netip.Prefix) {

if t == nil {
return
}

pfx = new(netip.Prefix)
*pfx = netip.Prefix(*t)

return
}

// UnmarshalText populates a TunPrefix from a text representation.
func (t *TunPrefix) UnmarshalText(b []byte) (err error) {

var pfx netip.Prefix

if b == nil || len(b) == 0 {
return
}

if pfx, err = netip.ParsePrefix(string(b)); err != nil {
return
}

*t = TunPrefix(pfx)

return
}

// Value conforms a TunPrefix to a sql/driver.Valuer interface. It returns val from t.
func (t TunPrefix) Value() (val driver.Value, err error) {

val = t.ToPrefix().String()

return
}

View File

@ -9,10 +9,13 @@ import (
`r00t2.io/gobroke/conf`
)

type Client struct {
tunCfg *conf.Config
myAddr net.IP
}
/*
TunPrefix is derived from netip.Prefix.
Because even though -- EVEN THOUGH -- it has a TextMarshaler and TextUnmarshaler interface,
it fails to work properly because Golang.
https://github.com/jmoiron/sqlx/issues/957
*/
type TunPrefix netip.Prefix

type TunnelList struct {
XMLName xml.Name `json:"-" xml:"tunnels" yaml:"-"`
@ -27,23 +30,20 @@ type Tunnel struct {
ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"`
ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"`
ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"`
Routed64 netip.Prefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"`
Routed48 *netip.Prefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"`
Routed64 TunPrefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"`
Routed48 *TunPrefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"`
RDNS1 *string `json:"rdns_1,omitempty" xml:"rdns1,omitempty" yaml:"RDNS #1,omitempty" db:"rdns_1"`
RDNS2 *string `json:"rdns_2,omitempty" xml:"rdns2,omitempty" yaml:"RDNS #2,omitempty" db:"rdns_2"`
RDNS3 *string `json:"rdns_3,omitempty" xml:"rdns3,omitempty" yaml:"RDNS #3,omitempty" db:"rdns_3"`
RDNS4 *string `json:"rdns_4,omitempty" xml:"rdns4,omitempty" yaml:"RDNS #4,omitempty" db:"rdns_4"`
RDNS5 *string `json:"rdns_5,omitempty" xml:"rdns5,omitempty" yaml:"RDNS #5,omitempty" db:"rdns_5"`
client *Client
heClient *resty.Client
tunCfg *conf.Tunnel
client *resty.Client
}

type FetchedIP struct {
NewClientIPv4 net.IP `json:"new_client_v4" xml:"newClientv4,attr" yaml:"Evaluated IPv4 Client Address" db:"client_ip"`
}

type FetchedTunnel struct {
*Tunnel
*FetchedIP
type HTTPError struct {
Code int `json:"code" xml:"code,attr" yaml:"Status Code"`
CodeStr string `json:"code_str" xml:"code_str,attr" yaml:"Status Code (Detailed)"`
Message *string `json:"message,omitempty" xml:",chardata" yaml:"Error Message,omitempty"`
Resp *resty.Response `json:"-" xml:"-" yaml:"-"`
}