update config, demo letsencrypt

This commit is contained in:
AJ ONeal 2020-05-04 22:12:11 -06:00
parent 3f9e05e998
commit 94c3680333
9 changed files with 155 additions and 71 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@ certs
/telebit /telebit
/cmd/telebit/telebit /cmd/telebit/telebit
/debug /debug
log.txt
*.log

View File

@ -1,4 +1,4 @@
# RVPN Server # Telebit
## branch: load-balancing ## branch: load-balancing

View File

@ -10,6 +10,7 @@ import (
golog "log" golog "log"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"git.coolaj86.com/coolaj86/go-telebitd/log" "git.coolaj86.com/coolaj86/go-telebitd/log"
@ -17,8 +18,9 @@ import (
"git.coolaj86.com/coolaj86/go-telebitd/relay/api" "git.coolaj86.com/coolaj86/go-telebitd/relay/api"
"git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy" "git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy"
"github.com/caddyserver/certmagic"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/spf13/viper" "github.com/go-acme/lego/v3/providers/dns/duckdns"
lumberjack "gopkg.in/natefinch/lumberjack.v2" lumberjack "gopkg.in/natefinch/lumberjack.v2"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
@ -49,22 +51,62 @@ var (
idle int idle int
dwell int dwell int
cancelcheck int cancelcheck int
lbDefaultMethod string lbDefaultMethod api.LoadBalanceStrategy
nickname string nickname string
acmeEmail string
acmeStorage string
acmeAgree bool
acmeStaging bool
allclients string
adminDomain string
wssDomain string
) )
func init() { func init() {
flag.StringVar(&logfile, "log", logfile, "Log file (or stdout/stderr; empty for none)") flag.StringVar(&allclients, "clients", "", "list of client:secret pairings such as example.com:secret123,foo.com:secret321")
flag.StringVar(&acmeEmail, "acme-email", "", "email to use for Let's Encrypt / ACME registration")
flag.StringVar(&acmeStorage, "acme-storage", "./acme.d/", "path to ACME storage directory")
flag.StringVar(&acmeStorage, "acme-storage", "./acme.d/", "path to ACME storage directory")
flag.BoolVar(&acmeAgree, "acme-agree", false, "agree to the terms of the ACME service provider (required)")
flag.BoolVar(&acmeStaging, "staging", false, "get fake certificates for testing")
flag.StringVar(&adminDomain, "admin-domain", "", "the management domain")
flag.StringVar(&wssDomain, "wss-domain", "", "the wss domain for connecting devices, if different from admin")
flag.StringVar(&configPath, "config-path", configPath, "Configuration File Path") flag.StringVar(&configPath, "config-path", configPath, "Configuration File Path")
flag.StringVar(&secretKey, "secret", "", "a >= 16-character random string for JWT key signing") flag.StringVar(&secretKey, "secret", "", "a >= 16-character random string for JWT key signing") // SECRET
flag.StringVar(&logfile, "log", logfile, "Log file (or stdout/stderr; empty for none)")
flag.IntVar(&tcpPort, "port", 0, "tcp port on which to listen") // PORT
flag.StringVar(&nickname, "nickname", "", "a nickname for this server, as an identifier") // NICKNAME
} }
var logoutput io.Writer var logoutput io.Writer
// Client is a domain and secret pair
type Client struct {
domain string
secret string
}
//Main -- main entry point //Main -- main entry point
func main() { func main() {
flag.Parse() flag.Parse()
if !acmeAgree {
fmt.Fprintf(os.Stderr, "set --acme-agree=true to accept the terms of the ACME service provider.\n")
os.Exit(1)
}
clients := []Client{}
for _, pair := range strings.Split(allclients, ", ") {
if len(pair) > 0 {
continue
}
keyval := strings.Split(pair, ":")
clients = append(clients, Client{
domain: keyval[0],
secret: keyval[1],
})
}
if "" == secretKey { if "" == secretKey {
secretKey = os.Getenv("TELEBIT_SECRET") secretKey = os.Getenv("TELEBIT_SECRET")
} }
@ -92,51 +134,73 @@ func main() {
// send the output io.Writing to the other packages // send the output io.Writing to the other packages
log.InitLogging(logoutput) log.InitLogging(logoutput)
viper.SetConfigName(configFile)
viper.AddConfigPath(configPath)
viper.AddConfigPath("./")
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("fatal error config file: %s", err))
}
flag.IntVar(&argDeadTime, "dead-time-counter", 5, "deadtime counter in seconds") flag.IntVar(&argDeadTime, "dead-time-counter", 5, "deadtime counter in seconds")
wssHostName = viper.Get("rvpn.wssdomain").(string) if 0 == tcpPort {
adminHostName = viper.Get("rvpn.admindomain").(string) tcpPort, err = strconv.Atoi(os.Getenv("PORT"))
tcpPort = viper.GetInt("rvpn.port") if nil != err {
deadtime := viper.Get("rvpn.deadtime").(map[string]interface{}) fmt.Fprintf(os.Stderr, "must specify --port or PORT\n")
idle = deadtime["idle"].(int) os.Exit(1)
dwell = deadtime["dwell"].(int) }
cancelcheck = deadtime["cancelcheck"].(int) }
lbDefaultMethod = viper.Get("rvpn.loadbalancing.defaultmethod").(string)
nickname = viper.Get("rvpn.serverName").(string) adminHostName = adminDomain
if 0 == len(adminHostName) {
adminHostName = os.Getenv("ADMIN_DOMAIN")
}
wssHostName = wssDomain
if 0 == len(wssHostName) {
wssHostName = os.Getenv("WSS_DOMAIN")
}
if 0 == len(wssHostName) {
wssHostName = adminHostName
}
// load balancer method
lbDefaultMethod = api.RoundRobin
if 0 == len(nickname) {
nickname = os.Getenv("NICKNAME")
}
// TODO what do these "deadtimes" do exactly?
dwell := 120
idle := 60
cancelcheck := 10
Loginfo.Println("startup") Loginfo.Println("startup")
ctx, cancelContext := context.WithCancel(context.Background()) ctx, cancelContext := context.WithCancel(context.Background())
defer cancelContext() defer cancelContext()
// CertMagic is Greenlock for Go
directory := certmagic.LetsEncryptProductionCA
if acmeStaging {
directory = certmagic.LetsEncryptStagingCA
}
magic, err := newCertMagic(directory, acmeEmail, &certmagic.FileStorage{Path: acmeStorage})
serverStatus := api.NewStatus(ctx) serverStatus := api.NewStatus(ctx)
serverStatus.AdminDomain = adminHostName serverStatus.AdminDomain = adminHostName
serverStatus.WssDomain = wssHostName serverStatus.WssDomain = wssHostName
serverStatus.Name = nickname serverStatus.Name = nickname
serverStatus.DeadTime = api.NewStatusDeadTime(dwell, idle, cancelcheck) serverStatus.DeadTime = api.NewStatusDeadTime(dwell, idle, cancelcheck)
serverStatus.LoadbalanceDefaultMethod = lbDefaultMethod serverStatus.LoadbalanceDefaultMethod = string(lbDefaultMethod)
connectionTable := api.NewTable(dwell, idle, lbDefaultMethod) connectionTable := api.NewTable(dwell, idle, lbDefaultMethod)
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
certbundle, err := magic.GetCertificate(hello)
// TODO // TODO
// 1. call out to greenlock for validation // 1. call out to greenlock for validation
// 2. push challenges through http channel // 2. push challenges through http channel
// 3. receive certificates (or don't) // 3. receive certificates (or don't)
certbundle, err := tls.LoadX509KeyPair("certs/fullchain.pem", "certs/privkey.pem") //certbundle, err := tls.LoadX509KeyPair("certs/fullchain.pem", "certs/privkey.pem")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &certbundle, nil return certbundle, nil
}, },
} }
@ -188,3 +252,46 @@ func main() {
r := relay.New(ctx, tlsConfig, authorizer, serverStatus, connectionTable) r := relay.New(ctx, tlsConfig, authorizer, serverStatus, connectionTable)
r.ListenAndServe(tcpPort) r.ListenAndServe(tcpPort)
} }
func newCertMagic(directory string, email string, storage certmagic.Storage) (*certmagic.Config, error) {
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
// do whatever you need to do to get the right
// configuration for this certificate; keep in
// mind that this config value is used as a
// template, and will be completed with any
// defaults that are set in the Default config
return &certmagic.Config{}, nil
},
})
provider, err := newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN"))
if err != nil {
return nil, err
}
magic := certmagic.New(cache, certmagic.Config{
Storage: storage,
OnDemand: &certmagic.OnDemandConfig{
DecisionFunc: func(name string) error {
return nil
},
},
})
// Ummm... just a little confusing
magic.Issuer = certmagic.NewACMEManager(magic, certmagic.ACMEManager{
DNSProvider: provider,
CA: directory,
Email: email,
Agreed: true,
DisableHTTPChallenge: true,
DisableTLSALPNChallenge: true,
// plus any other customizations you need
})
return magic, nil
}
// newDuckDNSProvider is for the sake of demoing the tunnel
func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) {
config := duckdns.NewDefaultConfig()
config.Token = token
return duckdns.NewDNSProviderConfig(config)
}

5
examples/example.env Normal file
View File

@ -0,0 +1,5 @@
NICKNAME=server-1
ADMIN_HOSTNAME=example.duckdns.org
WSS_HOSTNAME=
PORT=443
DUCKDNS_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

View File

@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"git.coolaj86.com/coolaj86/go-telebitd/relay/api" "git.coolaj86.com/coolaj86/go-telebitd/relay/api"
"git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy" "git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy"
@ -47,23 +46,9 @@ func ListenAndServe(mx *mplexy.MPlexy, adminListener net.Listener) error {
switch url := r.URL.Path; url { switch url := r.URL.Path; url {
case "/": case "/":
var hostname string http.Redirect(w, r, "/admin", 301)
host := strings.Split(r.Host, ":") serverStatus.AdminStats.IncResponses()
if len(host) > 0 { return
hostname = host[0]
}
// check to see if we are using the administrative Host
if hostname == mplexy.InvalidAdminDomain {
http.Redirect(w, r, "/admin", 301)
serverStatus.AdminStats.IncResponses()
return
}
if hostname == mx.AdminDomain() {
http.Redirect(w, r, "/admin", 301)
serverStatus.AdminStats.IncResponses()
return
}
default: default:
http.Error(w, "Not Found", 404) http.Error(w, "Not Found", 404)
} }

View File

@ -6,10 +6,12 @@ import (
"sync" "sync"
) )
type LoadBalanceStrategy string
const ( const (
lbmUnSupported string = "unsuported" UnSupported LoadBalanceStrategy = "unsuported"
lbmRoundRobin string = "round-robin" RoundRobin LoadBalanceStrategy = "round-robin"
lbmLeastConnections string = "least-connections" LeastConnections LoadBalanceStrategy = "least-connections"
) )
//DomainLoadBalance -- Use as a structure for domain connections //DomainLoadBalance -- Use as a structure for domain connections
@ -19,7 +21,7 @@ type DomainLoadBalance struct {
mutex sync.Mutex mutex sync.Mutex
//lb method, supported round robin. //lb method, supported round robin.
method string method LoadBalanceStrategy
//the last connection based on calculation //the last connection based on calculation
lastmember int lastmember int
@ -35,7 +37,7 @@ type DomainLoadBalance struct {
} }
//NewDomainLoadBalance -- Constructor //NewDomainLoadBalance -- Constructor
func NewDomainLoadBalance(defaultMethod string) (p *DomainLoadBalance) { func NewDomainLoadBalance(defaultMethod LoadBalanceStrategy) (p *DomainLoadBalance) {
p = new(DomainLoadBalance) p = new(DomainLoadBalance)
p.method = defaultMethod p.method = defaultMethod
p.lastmember = 0 p.lastmember = 0
@ -56,7 +58,7 @@ func (p *DomainLoadBalance) NextMember() (conn *Connection) {
//check for round robin, if not RR then drop out and call calculate //check for round robin, if not RR then drop out and call calculate
log.Println("NextMember:", p) log.Println("NextMember:", p)
if p.method == lbmRoundRobin { if p.method == RoundRobin {
p.lastmember++ p.lastmember++
if p.lastmember >= p.count { if p.lastmember >= p.count {
p.lastmember = 0 p.lastmember = 0

View File

@ -25,7 +25,7 @@ func NewStatusAPI(c *Status) (s *StatusAPI) {
s.Uptime = time.Since(c.StartTime).Seconds() s.Uptime = time.Since(c.StartTime).Seconds()
s.WssDomain = c.WssDomain s.WssDomain = c.WssDomain
s.AdminDomain = c.AdminDomain s.AdminDomain = c.AdminDomain
s.LoadbalanceDefaultMethod = c.LoadbalanceDefaultMethod s.LoadbalanceDefaultMethod = string(c.LoadbalanceDefaultMethod)
s.DeadTime = NewStatusDeadTimeAPI(c.DeadTime.dwell, c.DeadTime.idle, c.DeadTime.Cancelcheck) s.DeadTime = NewStatusDeadTimeAPI(c.DeadTime.dwell, c.DeadTime.idle, c.DeadTime.Cancelcheck)
s.AdminStats = NewTrafficAPI(c.AdminStats.Requests, c.AdminStats.Responses, c.AdminStats.BytesIn, c.AdminStats.BytesOut) s.AdminStats = NewTrafficAPI(c.AdminStats.Requests, c.AdminStats.Responses, c.AdminStats.BytesIn, c.AdminStats.BytesOut)
s.TrafficStats = NewTrafficAPI(c.TrafficStats.Requests, c.TrafficStats.Responses, c.TrafficStats.BytesIn, c.TrafficStats.BytesOut) s.TrafficStats = NewTrafficAPI(c.TrafficStats.Requests, c.TrafficStats.Responses, c.TrafficStats.BytesIn, c.TrafficStats.BytesOut)

View File

@ -22,11 +22,11 @@ type Table struct {
domainRevoke chan *DomainMapping domainRevoke chan *DomainMapping
dwell int dwell int
idle int idle int
balanceMethod string balanceMethod LoadBalanceStrategy
} }
//NewTable -- consructor //NewTable -- consructor
func NewTable(dwell, idle int, balanceMethod string) (p *Table) { func NewTable(dwell, idle int, balanceMethod LoadBalanceStrategy) (p *Table) {
p = new(Table) p = new(Table)
p.connections = make(map[*Connection][]string) p.connections = make(map[*Connection][]string)
p.Domains = make(map[string]*DomainLoadBalance) p.Domains = make(map[string]*DomainLoadBalance)

View File

@ -1,18 +0,0 @@
rvpn:
serverName: rvpn1
wssdomain: localhost.rootprojects.org
admindomain: rvpn.rootprojects.invalid
genericlistener: 8443
deadtime:
dwell: 120
idle: 60
cancelcheck: 10
domains:
test.rootprojects.org:
secret: abc123
test2.rootprojects.org:
secret: abc123
test3.rootprojects.org:
secret: abc123
loadbalancing:
defaultmethod: round-robin