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
24 changed files with 941 additions and 112 deletions

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