diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..5b9f750 --- /dev/null +++ b/README.adoc @@ -0,0 +1,23 @@ += GoBroke +r00t^2 +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. diff --git a/README.md b/README.md deleted file mode 100644 index 6613a42..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# GoBroke - -A Golang tunnelbroker.net client/manager. \ No newline at end of file diff --git a/cachedb/_static/cache.schema.sql b/cachedb/_static/cache.schema.sql index 36cc313..011312b 100644 --- a/cachedb/_static/cache.schema.sql +++ b/cachedb/_static/cache.schema.sql @@ -1,31 +1,43 @@ -PRAGMA foreign_keys=OFF; +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 INTEGER NOT NULL, - checked INTEGER NOT NULL, - updated INTEGER +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 INTEGER NOT NULL, when_fetched INTEGER, - CONSTRAINT client_ips_tunnels_FK FOREIGN KEY (tun_id) REFERENCES tunnels(tun_id) ON DELETE CASCADE ON UPDATE CASCADE +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 ); -INSERT INTO sqlite_sequence VALUES('client_ips',0); COMMIT; -PRAGMA foreign_keys=ON; +PRAGMA foreign_keys= ON; diff --git a/cachedb/consts.go b/cachedb/consts.go index a640da5..ee3fb72 100644 --- a/cachedb/consts.go +++ b/cachedb/consts.go @@ -8,8 +8,3 @@ var ( //go:embed "_static/cache.schema.sql" schemaBytes []byte ) - -const ( - SelectTunnels string = "" - SelectTunnelById string = "" -) diff --git a/cachedb/funcs.go b/cachedb/funcs.go index a6e80a8..0f4557b 100644 --- a/cachedb/funcs.go +++ b/cachedb/funcs.go @@ -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 + } + } + 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 } } - // TODO + + 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 } diff --git a/cachedb/funcs_cache.go b/cachedb/funcs_cache.go new file mode 100644 index 0000000..22b385f --- /dev/null +++ b/cachedb/funcs_cache.go @@ -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 +} diff --git a/cachedb/types.go b/cachedb/types.go index 1dbc277..3d818ab 100644 --- a/cachedb/types.go +++ b/cachedb/types.go @@ -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 - Set time.Time `db:"when_set"` +/* + 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"` } diff --git a/conf/_testdata/test.toml b/conf/_testdata/test.toml index ca9b987..d90f753 100644 --- a/conf/_testdata/test.toml +++ b/conf/_testdata/test.toml @@ -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. diff --git a/conf/_testdata/test_uncommented.toml b/conf/_testdata/test_uncommented.toml new file mode 100644 index 0000000..c836043 --- /dev/null +++ b/conf/_testdata/test_uncommented.toml @@ -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" diff --git a/conf/funcs.go b/conf/funcs.go index da2b453..5198193 100644 --- a/conf/funcs.go +++ b/conf/funcs.go @@ -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 + } + } } } } diff --git a/conf/funcs_perms.go b/conf/funcs_perms.go new file mode 100644 index 0000000..e231776 --- /dev/null +++ b/conf/funcs_perms.go @@ -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 +} diff --git a/conf/funcs_permspec.go b/conf/funcs_permspec.go new file mode 100644 index 0000000..3fc45a0 --- /dev/null +++ b/conf/funcs_permspec.go @@ -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 +} diff --git a/conf/types.go b/conf/types.go index 5fbf881..6dd395d 100644 --- a/conf/types.go +++ b/conf/types.go @@ -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 } diff --git a/go.mod b/go.mod index 1a9bdf9..fbf9f2c 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index c0710b4..49a3710 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/runner/types.go b/runner/types.go new file mode 100644 index 0000000..fc685eb --- /dev/null +++ b/runner/types.go @@ -0,0 +1,11 @@ +package runner + +import ( + `r00t2.io/gobroke/cachedb` + `r00t2.io/gobroke/conf` +) + +type Updater struct { + cfg *conf.Config + cache *cachedb.Cache +} diff --git a/tunnelbroker/consts.go b/tunnelbroker/consts.go index 948530d..431de7b 100644 --- a/tunnelbroker/consts.go +++ b/tunnelbroker/consts.go @@ -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" ) diff --git a/tunnelbroker/errs.go b/tunnelbroker/errs.go new file mode 100644 index 0000000..e1b69aa --- /dev/null +++ b/tunnelbroker/errs.go @@ -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") +) diff --git a/tunnelbroker/funcs.go b/tunnelbroker/funcs.go index a1678a5..ab70029 100644 --- a/tunnelbroker/funcs.go +++ b/tunnelbroker/funcs.go @@ -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 } diff --git a/tunnelbroker/funcs_httperror.go b/tunnelbroker/funcs_httperror.go new file mode 100644 index 0000000..dedc5d0 --- /dev/null +++ b/tunnelbroker/funcs_httperror.go @@ -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 +} diff --git a/tunnelbroker/funcs_test.go b/tunnelbroker/funcs_test.go new file mode 100644 index 0000000..b45135b --- /dev/null +++ b/tunnelbroker/funcs_test.go @@ -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)) +} diff --git a/tunnelbroker/funcs_tunnel.go b/tunnelbroker/funcs_tunnel.go new file mode 100644 index 0000000..1ade751 --- /dev/null +++ b/tunnelbroker/funcs_tunnel.go @@ -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 +} diff --git a/tunnelbroker/funcs_tunprefix.go b/tunnelbroker/funcs_tunprefix.go new file mode 100644 index 0000000..16208a0 --- /dev/null +++ b/tunnelbroker/funcs_tunprefix.go @@ -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 +} diff --git a/tunnelbroker/types.go b/tunnelbroker/types.go index ef4e542..2f64742 100644 --- a/tunnelbroker/types.go +++ b/tunnelbroker/types.go @@ -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:"-"` @@ -20,30 +23,27 @@ type TunnelList struct { } type Tunnel struct { - XMLName xml.Name `json:"-" xml:"tunnel" yaml:"-"` - ID uint `json:"id" xml:"id,attr" yaml:"ID" db:"tun_id"` - Description string `json:"desc" xml:"description" yaml:"Description" db:"desc"` - ServerIPv4 net.IP `json:"tgt_v4" xml:"serverv4" yaml:"IPv4 Tunnel Target" db:"server_v4"` - ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"` - ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"` - ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"` - Routed64 netip.Prefix `json:"routed_64" xml:"routed64" yaml:"Routed /64" db:"prefix_64"` - Routed48 *netip.Prefix `json:"routed_48,omitempty" xml:"routed48,omitempty" yaml:"Routed /48,omitempty" db:"prefix_48"` - RDNS1 *string `json:"rdns_1,omitempty" xml:"rdns1,omitempty" yaml:"RDNS #1,omitempty" db:"rdns_1"` - RDNS2 *string `json:"rdns_2,omitempty" xml:"rdns2,omitempty" yaml:"RDNS #2,omitempty" db:"rdns_2"` - RDNS3 *string `json:"rdns_3,omitempty" xml:"rdns3,omitempty" yaml:"RDNS #3,omitempty" db:"rdns_3"` - RDNS4 *string `json:"rdns_4,omitempty" xml:"rdns4,omitempty" yaml:"RDNS #4,omitempty" db:"rdns_4"` - RDNS5 *string `json:"rdns_5,omitempty" xml:"rdns5,omitempty" yaml:"RDNS #5,omitempty" db:"rdns_5"` - client *Client - heClient *resty.Client + XMLName xml.Name `json:"-" xml:"tunnel" yaml:"-"` + ID uint `json:"id" xml:"id,attr" yaml:"ID" db:"tun_id"` + Description string `json:"desc" xml:"description" yaml:"Description" db:"desc"` + ServerIPv4 net.IP `json:"tgt_v4" xml:"serverv4" yaml:"IPv4 Tunnel Target" db:"server_v4"` + ClientIPv4 net.IP `json:"client_v4" xml:"clientv4" yaml:"Configured IPv4 Client Address" db:"current_client_v4"` + ServerIPv6 net.IP `json:"server_v6" xml:"serverv6" yaml:"IPv6 Endpoint" db:"tunnel_server_v6"` + ClientIPv6 net.IP `json:"client_v6" xml:"clientv6" yaml:"IPv6 Tunnel Client Address" db:"tunnel_client_v6"` + Routed64 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"` 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:"-"` }