Skip to content

Commit

Permalink
Support multiple domain names (#153)
Browse files Browse the repository at this point in the history
* Introduce support for multiple domain names

* Use latest iprose version

* Remove obsoleted comment

* Fix typo

* Fix response when no way to get hostname

* Remove explicit ciphers initialization
  • Loading branch information
RandoMan70 committed Apr 27, 2024
1 parent b29a7de commit 20e682c
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 366 deletions.
37 changes: 13 additions & 24 deletions cmd/tunnel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,48 +163,37 @@ func initServices(runtime *runtime.TunnelRuntime) error {
if runtime.Settings.HTTP.CORS {
xhttpOpts = append([]xhttp.Option{xhttp.WithCORS()}, xhttpOpts...)
}
// assume that config validation does not pass
// the SSL enabled without the domain name configuration

if runtime.Settings.SSL != nil {
redirectOnly := xhttp.NewRedirectToSSL(runtime.Settings.Domain.Name)
redirectOnly := xhttp.NewRedirectToSSL(runtime.Settings.Domain.PrimaryName)
// we must start the redirect-only server before passing its Router
// to the certificate issuer.
if err := redirectOnly.Run(runtime.Settings.HTTP.ListenAddr); err != nil {
return err
}
runtime.Services.RegisterService("httpRedirectServer", redirectOnly)

opts := xhttp.IssuerOpts{
Domain: runtime.Settings.Domain.Name,
CacheDir: runtime.Settings.Domain.Dir,
Router: redirectOnly.Router(),

// Callback handles the xhttp.Server restarts on certificate updates
Callback: func(c *tls.Config) {
newHttp := xhttp.NewDefaultSSL(c)
if err := newHttp.Run(runtime.Settings.SSL.ListenAddr); err != nil {
zap.L().Fatal("failed to start new https server", zap.Error(err))
}
if err := runtime.Services.Replace("httpServer", newHttp); err != nil {
zap.L().Fatal("failed to replace the httpServer service", zap.Error(err))
}
},
}
issuer, err := xhttp.NewIssuer(opts)
if err != nil {
return err
opts := &xhttp.CertMasterOpts{
Email: runtime.Settings.Domain.Email,
CacheDir: runtime.Settings.Domain.Dir,
NonSSLRouter: redirectOnly.Router(),
Domains: append([]string{runtime.Settings.Domain.PrimaryName}, runtime.Settings.Domain.ExtraNames...),
}

tlscfg, err := issuer.TLSConfig()
certMaster, err := xhttp.NewCertMaster(opts)
if err != nil {
return err
}
runtime.Services.RegisterService("certMaster", certMaster)
tlsCfg := &tls.Config{
GetCertificate: certMaster.GetCertificate,
}

// store the plaintext http router to use for
// solve the http01 challenge while updating Settings
runtime.HttpRouter = redirectOnly.Router()
xHttpAddr = runtime.Settings.SSL.ListenAddr
xhttpOpts = append([]xhttp.Option{xhttp.WithSSL(tlscfg)}, xhttpOpts...)
xhttpOpts = append([]xhttp.Option{xhttp.WithSSL(tlsCfg)}, xhttpOpts...)
}

xHttpServer := xhttp.New(xhttpOpts...)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ require (
github.com/stretchr/testify v1.8.1
github.com/vishvananda/netlink v1.1.0
github.com/vpnhouse/api v0.0.0-20240207081757-572ff9ef5b97
github.com/vpnhouse/iprose-go v0.1.0-rc12
github.com/vpnhouse/iprose-go v0.1.0-rc13
go.etcd.io/etcd/client/v3 v3.5.2
go.uber.org/multierr v1.10.0
go.uber.org/zap v1.25.0
golang.org/x/net v0.2.0
golang.org/x/sys v0.11.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211230205640-daad0b7ba671
google.golang.org/grpc v1.44.0
Expand Down Expand Up @@ -91,7 +92,6 @@ require (
go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.org/x/tools v0.3.0 // indirect
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -815,10 +815,8 @@ github.com/vpnhouse/api v0.0.0-20240207081757-572ff9ef5b97 h1:FwO9Yeqikw7MxnUESA
github.com/vpnhouse/api v0.0.0-20240207081757-572ff9ef5b97/go.mod h1:qeAZBOFAiz7FiTG49UremjHQExCUPui2tTdn1NMDn1s=
github.com/vpnhouse/iprose-go v0.0.6 h1:UC2ozImvlt2d0nvB+r9xNtSJhQ07NFEBRb126BDCAeQ=
github.com/vpnhouse/iprose-go v0.0.6/go.mod h1:YtGgxhjsyG8LXc2N1SF6/YM1ndkU5l5T7mvT4dunUE8=
github.com/vpnhouse/iprose-go v0.1.0-rc11 h1:KkOGRPvB4UHdWvV0hScJbN8dXe84BLTGWrXn7bRvK0I=
github.com/vpnhouse/iprose-go v0.1.0-rc11/go.mod h1:0tRDoa2c7uD4nboPAs+LDztyAE+5SFgn5uqdhZ2VYfg=
github.com/vpnhouse/iprose-go v0.1.0-rc12 h1:3JE2z2FNEur/kjKUESX7EWoyED320H0Et09whF5agQk=
github.com/vpnhouse/iprose-go v0.1.0-rc12/go.mod h1:0tRDoa2c7uD4nboPAs+LDztyAE+5SFgn5uqdhZ2VYfg=
github.com/vpnhouse/iprose-go v0.1.0-rc13 h1:OnDLt4j+116/HUDcAvEdP86uZueS6RcJXKSiHVb9GCE=
github.com/vpnhouse/iprose-go v0.1.0-rc13/go.mod h1:0tRDoa2c7uD4nboPAs+LDztyAE+5SFgn5uqdhZ2VYfg=
github.com/vultr/govultr/v2 v2.7.1/go.mod h1:BvOhVe6/ZpjwcoL6/unkdQshmbS9VGbowI4QT+3DGVU=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
Expand Down
58 changes: 14 additions & 44 deletions internal/httpapi/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package httpapi

import (
"crypto/tls"
"encoding/json"
"net"
"net/http"
Expand All @@ -20,7 +19,6 @@ import (
"github.com/vpnhouse/tunnel/pkg/xerror"
"github.com/vpnhouse/tunnel/pkg/xhttp"
"github.com/vpnhouse/tunnel/pkg/xnet"
"go.uber.org/zap"
)

// AdminGetSettings implements handler for GET /api/tunnel/admin/settings request
Expand Down Expand Up @@ -49,11 +47,11 @@ func (tun *TunnelAPI) AdminInitialSetup(w http.ResponseWriter, r *http.Request)
var dc *xhttp.DomainConfig = nil
if req.Domain != nil {
dc = &xhttp.DomainConfig{
Mode: string(req.Domain.Mode),
Name: req.Domain.DomainName,
IssueSSL: req.Domain.IssueSsl,
Schema: string(req.Domain.Schema),
Dir: tun.runtime.Settings.ConfigDir(),
Mode: string(req.Domain.Mode),
PrimaryName: req.Domain.DomainName,
IssueSSL: req.Domain.IssueSsl,
Schema: string(req.Domain.Schema),
Dir: tun.runtime.Settings.ConfigDir(),
}
if err := dc.Validate(); err != nil {
return nil, err
Expand All @@ -65,12 +63,7 @@ func (tun *TunnelAPI) AdminInitialSetup(w http.ResponseWriter, r *http.Request)
return nil, err
}
tun.runtime.Settings.Wireguard.Subnet = validator.Subnet(subnet)
// blocks for a certificate issuing, timeout is a LE request timeout is about 10s
if needCert := setDomainConfig(tun.runtime.Settings, dc); needCert {
if err := tun.issueCertificateSync(); err != nil {
return nil, err
}
}
setDomainConfig(tun.runtime.Settings, dc)

// setting the password resets the "initial setup required" flag.
if err := tun.runtime.Settings.SetAdminPassword(req.AdminPassword); err != nil {
Expand Down Expand Up @@ -123,7 +116,7 @@ func settingsToOpenAPI(s *settings.Config) adminAPI.Settings {
var dc *adminAPI.DomainConfig = nil
if s.Domain != nil {
dc = &adminAPI.DomainConfig{
DomainName: s.Domain.Name,
DomainName: s.Domain.PrimaryName,
IssueSsl: s.Domain.IssueSSL,
Mode: adminAPI.DomainConfigMode(s.Domain.Mode),
Schema: adminAPI.DomainConfigSchema(s.Domain.Schema),
Expand Down Expand Up @@ -180,21 +173,16 @@ func (tun *TunnelAPI) mergeStaticSettings(rt *runtime.TunnelRuntime, s adminAPI.
}
if s.Domain != nil {
tmpDC := &xhttp.DomainConfig{
Name: s.Domain.DomainName,
Mode: string(s.Domain.Mode),
IssueSSL: s.Domain.IssueSsl,
Schema: string(s.Domain.Schema),
PrimaryName: s.Domain.DomainName,
Mode: string(s.Domain.Mode),
IssueSSL: s.Domain.IssueSsl,
Schema: string(s.Domain.Schema),
}
if err := tmpDC.Validate(); err != nil {
return err
}

if needCert := setDomainConfig(tun.runtime.Settings, tmpDC); needCert {
// blocks for a certificate issuing, timeout is a LE request timeout is about 10s
if err := tun.issueCertificateSync(); err != nil {
return err
}
}
setDomainConfig(tun.runtime.Settings, tmpDC)
} else {
// consider "domain: null" as "disabled for the whole option set"
rt.Settings.Domain = nil
Expand All @@ -220,24 +208,6 @@ func (tun *TunnelAPI) mergeStaticSettings(rt *runtime.TunnelRuntime, s adminAPI.
return nil
}

func (tun *TunnelAPI) issueCertificateSync() error {
issuer, err := xhttp.NewIssuer(xhttp.IssuerOpts{
Domain: tun.runtime.Settings.Domain.Name,
CacheDir: tun.runtime.Settings.ConfigDir(),
Router: tun.runtime.HttpRouter,
Callback: func(_ *tls.Config) {
zap.L().Info("ssl certificate issued", zap.String("name", tun.runtime.Settings.Domain.Name))
},
})
if err != nil {
return err
}

// ask for the config (it will be cached inside and re-used after the restart).
_, err = issuer.TLSConfig()
return err
}

// setDomainConfig updates current settings with new domain config,
// return true if the new certificate must be issued.
func setDomainConfig(c *settings.Config, dc *xhttp.DomainConfig) bool {
Expand All @@ -248,7 +218,7 @@ func setDomainConfig(c *settings.Config, dc *xhttp.DomainConfig) bool {
oldName := ""
if c.Domain != nil {
if c.Domain.Mode == string(adminAPI.DomainConfigModeDirect) {
oldName = c.Domain.Name
oldName = c.Domain.PrimaryName
}
}

Expand All @@ -263,7 +233,7 @@ func setDomainConfig(c *settings.Config, dc *xhttp.DomainConfig) bool {
}
}
// notify caller that the name differs
return dc.Name != oldName
return dc.PrimaryName != oldName
}

return false
Expand Down
20 changes: 10 additions & 10 deletions internal/httpapi/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,32 @@ func TestSetDomainConfig(t *testing.T) {
},
{
c: &C{},
dc: &DC{Mode: _direct, Name: "foo.com"},
dc: &DC{Mode: _direct, PrimaryName: "foo.com"},
update: false, // no issue_ssl here
},
{
c: &C{},
dc: &DC{Mode: _direct, IssueSSL: true, Name: "foo.com"},
dc: &DC{Mode: _direct, IssueSSL: true, PrimaryName: "foo.com"},
update: true, // certificate requested
},
{
c: &C{Domain: &DC{Mode: "wat", Name: "old.example.org"}},
dc: &DC{Mode: _direct, Name: "new.example.org"},
c: &C{Domain: &DC{Mode: "wat", PrimaryName: "old.example.org"}},
dc: &DC{Mode: _direct, PrimaryName: "new.example.org"},
update: false, // name differs but SSL does not requested
},
{
c: &C{Domain: &DC{Mode: _direct, IssueSSL: true, Name: "old.example.org"}},
dc: &DC{Mode: _direct, IssueSSL: false, Name: "new.example.org"},
c: &C{Domain: &DC{Mode: _direct, IssueSSL: true, PrimaryName: "old.example.org"}},
dc: &DC{Mode: _direct, IssueSSL: false, PrimaryName: "new.example.org"},
update: false, // new name, ssl now becomes disabled
},
{
c: &C{Domain: &DC{Mode: _direct, IssueSSL: true, Name: "old.example.org"}},
dc: &DC{Mode: _direct, IssueSSL: false, Name: "old.example.org"},
c: &C{Domain: &DC{Mode: _direct, IssueSSL: true, PrimaryName: "old.example.org"}},
dc: &DC{Mode: _direct, IssueSSL: false, PrimaryName: "old.example.org"},
update: false, // name is the same, but no ssl (wat?)
},
{
c: &C{Domain: &DC{Mode: _direct, IssueSSL: true, Name: "old.example.org"}},
dc: &DC{Mode: _direct, IssueSSL: true, Name: "new.example.org"},
c: &C{Domain: &DC{Mode: _direct, IssueSSL: true, PrimaryName: "old.example.org"}},
dc: &DC{Mode: _direct, IssueSSL: true, PrimaryName: "new.example.org"},
update: true, // new name, with ssl as well
},
}
Expand Down
8 changes: 4 additions & 4 deletions internal/settings/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,13 @@ func (s *Config) ConfigDir() string {
func (s *Config) PublicURL() string {
if s.Domain != nil {
if s.Domain.Mode == string(adminAPI.DomainConfigModeReverseProxy) {
return s.Domain.Schema + "://" + s.Domain.Name
return s.Domain.Schema + "://" + s.Domain.PrimaryName
}
}

host := s.Wireguard.ServerIPv4
if s.Domain != nil {
host = s.Domain.Name
host = s.Domain.PrimaryName
}

if s.SSL != nil {
Expand Down Expand Up @@ -319,8 +319,8 @@ func (s *Config) validate() error {
return xerror.EInternalError("ssl.listen_addr is required", nil)
}

if s.Domain == nil || len(s.Domain.Name) == 0 {
return xerror.EInternalError("SSL server is enabled, but no domain name is set", nil)
if s.Domain == nil || len(s.Domain.PrimaryName) == 0 {
return xerror.EInternalError("SSL server is enabled, but domain name is not set", nil)
}
}

Expand Down
12 changes: 6 additions & 6 deletions internal/settings/static_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ func TestXValidation(t *testing.T) {

c = &Config{
Domain: &xhttp.DomainConfig{
Mode: "direct",
Name: "the.foo.bar",
IssueSSL: true,
Mode: "direct",
PrimaryName: "the.foo.bar",
IssueSSL: true,
},
SSL: nil,
}
require.Error(t, c.validate())

c = &Config{
Domain: &xhttp.DomainConfig{
Mode: "reverse-proxy",
Name: "the.foo.bar",
Schema: "https",
Mode: "reverse-proxy",
PrimaryName: "the.foo.bar",
Schema: "https",
},
SSL: nil,
}
Expand Down
51 changes: 51 additions & 0 deletions pkg/xhttp/domain_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package xhttp

import (
adminAPI "github.com/vpnhouse/api/go/server/tunnel_admin"
"github.com/vpnhouse/tunnel/pkg/xerror"
)

// DomainConfig is the YAML version of the `adminAPI.DomainConfig` struct.
type DomainConfig struct {
Mode string `yaml:"mode" valid:"required"`
PrimaryName string `yaml:"name" valid:"dns,required"`
ExtraNames []string `yaml:"extra_names,omitempty" valid:"dns"`
IssueSSL bool `yaml:"issue_ssl,omitempty"`
Schema string `yaml:"schema,omitempty"`
Email string `yaml:"email,omitempty" valid:"email"`

// Dir to store cached certificates, use sub-directory of cfgDir if possible.
Dir string `yaml:"dir,omitempty" valid:"path"`
}

func (c *DomainConfig) Validate() error {
modes := map[string]struct{}{
string(adminAPI.DomainConfigModeReverseProxy): {},
string(adminAPI.DomainConfigModeDirect): {},
}
schemas := map[string]struct{}{
"http": {},
"https": {},
}

if len(c.PrimaryName) == 0 {
return xerror.EInternalError("domain.name is required", nil)
}
if len(c.Mode) == 0 {
return xerror.EInternalError("domain.mode is required", nil)
}
if _, ok := modes[c.Mode]; !ok {
return xerror.EInternalError("domain.mode got unknown value, expecting `direct` or `reverse-proxy`", nil)
}

if c.Mode == string(adminAPI.DomainConfigModeReverseProxy) {
if len(c.Schema) == 0 {
return xerror.EInternalError("domain.schema is required for the reverse-proxy mode", nil)
}
if _, ok := schemas[c.Schema]; !ok {
return xerror.EInternalError("domain.schema got unknown value, expecting `http` or `https`", nil)
}
}

return nil
}
Loading

0 comments on commit 20e682c

Please sign in to comment.