From 036793157b73a1ee9d55c4b2e5cfdd0433121545 Mon Sep 17 00:00:00 2001 From: m5r Date: Sun, 21 Jul 2024 22:59:32 +0200 Subject: [PATCH 01/11] wip --- Dockerfile | 3 +- certs/account.go | 34 +++++++++-------- certs/certs.go | 12 +++--- certs/config.go | 21 ----------- cmd/root.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 26 +++++++++++-- go.sum | 57 ++++++++++++++++++++++++++++ main.go | 30 +-------------- utils/config.go | 29 ++++++++++++++ xip/xip.go | 14 +++---- 10 files changed, 242 insertions(+), 82 deletions(-) delete mode 100644 certs/config.go create mode 100644 cmd/root.go create mode 100644 utils/config.go diff --git a/Dockerfile b/Dockerfile index 17a690b..994a62a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,5 @@ EXPOSE 443/tcp USER root -CMD ["/local-ip/local-ip"] +# TODO: make these configurable too +CMD ["/local-ip/local-ip", "--domain", "local-ip.sh", "--email", "admin@local-ip.sh", "--nameservers", "ns1.local-ip.sh.,ns2.local-ip.sh."] diff --git a/certs/account.go b/certs/account.go index f851285..614cf07 100644 --- a/certs/account.go +++ b/certs/account.go @@ -36,7 +36,8 @@ func (u *Account) GetPrivateKey() crypto.PrivateKey { } func LoadAccount() *Account { - jsonBytes, err := os.ReadFile(accountFilePath) + config := utils.GetConfig() + jsonBytes, err := os.ReadFile(config.AccountFilePath) if err != nil { if strings.Contains(err.Error(), "no such file or directory") { RegisterAccount() @@ -51,7 +52,7 @@ func LoadAccount() *Account { utils.Logger.Fatal().Err(err).Msg("Failed to unmarshal account JSON file") } - privKey, err := os.ReadFile(keyFilePath) + privKey, err := os.ReadFile(config.KeyFilePath) if err != nil { utils.Logger.Fatal().Err(err).Msg("Failed to read account's private key file") } @@ -61,41 +62,42 @@ func LoadAccount() *Account { } func RegisterAccount() { + config := utils.GetConfig() privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { utils.Logger.Fatal().Err(err).Msg("Failed to generate account key") } account := &Account{ - Email: email, + Email: config.Email, key: privateKey, } - config := lego.NewConfig(account) - config.CADirURL = caDirUrl - legoClient, err := lego.NewClient(config) + legoConfig := lego.NewConfig(account) + legoConfig.CADirURL = config.CADirURL + legoClient, err := lego.NewClient(legoConfig) if err != nil { - utils.Logger.Fatal().Err(err).Str("CA Directory URL", config.CADirURL).Msg("Failed to initialize lego client") + utils.Logger.Fatal().Err(err).Str("CA Directory URL", legoConfig.CADirURL).Msg("Failed to initialize lego client") } reg, err := legoClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { - utils.Logger.Fatal().Err(err).Str("CA Directory URL", config.CADirURL).Msg("Failed to register account to ACME server") + utils.Logger.Fatal().Err(err).Str("CA Directory URL", legoConfig.CADirURL).Msg("Failed to register account to ACME server") } if reg.Body.Status != "valid" { - utils.Logger.Fatal().Err(err).Str("CA Directory URL", config.CADirURL).Msgf("Registration failed with status %s", reg.Body.Status) + utils.Logger.Fatal().Err(err).Str("CA Directory URL", legoConfig.CADirURL).Msgf("Registration failed with status %s", reg.Body.Status) } utils.Logger.Debug(). - Str("CA Directory URL", config.CADirURL). + Str("CA Directory URL", legoConfig.CADirURL). Bool("TermsOfServiceAgreed", reg.Body.TermsOfServiceAgreed). Msg("Successfully registered account to ACME server") account.Registration = reg - os.MkdirAll(filepath.Dir(keyFilePath), os.ModePerm) + os.MkdirAll(filepath.Dir(config.KeyFilePath), os.ModePerm) privKey := encode(privateKey) - err = os.WriteFile(keyFilePath, []byte(privKey), 0o644) + err = os.WriteFile(config.KeyFilePath, []byte(privKey), 0o644) if err != nil { - utils.Logger.Fatal().Err(err).Str("path", keyFilePath).Msg("Failed to write account's private key file") + utils.Logger.Fatal().Err(err).Str("path", config.KeyFilePath).Msg("Failed to write account's private key file") } jsonBytes, err := json.MarshalIndent(account, "", "\t") @@ -103,10 +105,10 @@ func RegisterAccount() { utils.Logger.Fatal().Err(err).Msg("Failed to marshal account JSON file") } - os.MkdirAll(filepath.Dir(accountFilePath), os.ModePerm) - err = os.WriteFile(accountFilePath, jsonBytes, 0o600) + os.MkdirAll(filepath.Dir(config.AccountFilePath), os.ModePerm) + err = os.WriteFile(config.AccountFilePath, jsonBytes, 0o600) if err != nil { - utils.Logger.Fatal().Err(err).Str("path", accountFilePath).Msg("Failed to write account's JSON file") + utils.Logger.Fatal().Err(err).Str("path", config.AccountFilePath).Msg("Failed to write account's JSON file") } } diff --git a/certs/certs.go b/certs/certs.go index 960483f..814a61b 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -27,14 +27,15 @@ func (c *certsClient) RequestCertificates() { } func (c *certsClient) requestCertificate(certType string) { + config := utils.GetConfig() var lastCertificate *certificate.Resource var domains []string if certType == "wildcard" { lastCertificate = c.lastWildcardCertificate - domains = []string{"*.local-ip.sh"} + domains = []string{fmt.Sprintf("*.%s", config.Domain)} } else if certType == "root" { lastCertificate = c.lastRootCertificate - domains = []string{"local-ip.sh"} + domains = []string{config.Domain} } else { utils.Logger.Fatal().Msgf("Unexpected certType %s. Only \"wildcard\" and \"root\" are supported", certType) } @@ -119,9 +120,10 @@ func persistFiles(certificates *certificate.Resource, certType string) { } func NewCertsClient(xip *xip.Xip, user *Account) *certsClient { - config := lego.NewConfig(user) - config.CADirURL = caDirUrl - legoClient, err := lego.NewClient(config) + config := utils.GetConfig() + legoConfig := lego.NewConfig(user) + legoConfig.CADirURL = config.CADirURL + legoClient, err := lego.NewClient(legoConfig) if err != nil { utils.Logger.Fatal().Err(err).Msg("Failed to initialize lego client") } diff --git a/certs/config.go b/certs/config.go deleted file mode 100644 index b9577b1..0000000 --- a/certs/config.go +++ /dev/null @@ -1,21 +0,0 @@ -package certs - -import ( - "fmt" - "net/url" - - "github.com/go-acme/lego/v4/lego" -) - -const ( - email = "admin@local-ip.sh" - caDirUrl = lego.LEDirectoryProduction - // caDirUrl = lego.LEDirectoryStaging -) - -var ( - parsedCaDirUrl, _ = url.Parse(caDirUrl) - caDirHostname = parsedCaDirUrl.Hostname() - accountFilePath = fmt.Sprintf("./.lego/accounts/%s/%s/account.json", caDirHostname, email) - keyFilePath = fmt.Sprintf("./.lego/accounts/%s/%s/keys/%s.key", caDirHostname, email, email) -) diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..4845961 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/go-acme/lego/v4/lego" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "local-ip.sh/utils" +) + +var command = &cobra.Command{ + Use: "local-ip.sh", + PreRun: func(cmd *cobra.Command, args []string) { + staging, err := cmd.Flags().GetBool("staging") + if err != nil { + utils.Logger.Fatal().Err(err).Msg("Unexpected error") + } + + nameservers, err := cmd.Flags().GetString("nameservers") + if err != nil { + utils.Logger.Fatal().Err(err).Msg("Unexpected error") + } + viper.Set("NameServers", strings.Split(nameservers, ",")) + + var caDir string + if staging { + caDir = lego.LEDirectoryStaging + } else { + caDir = lego.LEDirectoryProduction + } + viper.Set("CADirURL", caDir) + + parsedCaDirUrl, _ := url.Parse(caDir) + caDirHostname := parsedCaDirUrl.Hostname() + email := viper.GetString("Email") + + viper.Set("AccountFilePath", fmt.Sprintf("./.lego/accounts/%s/%s/account.json", caDirHostname, email)) + viper.Set("KeyFilePath", fmt.Sprintf("./.lego/accounts/%s/%s/keys/%s.key", caDirHostname, email, email)) + + utils.InitConfig() + }, + Run: func(cmd *cobra.Command, args []string) { + c := utils.GetConfig() + fmt.Printf("%#v\n", c) + fmt.Println(c) + // n := xip.NewXip() + + // go func() { + // account := certs.LoadAccount() + // certsClient := certs.NewCertsClient(n, account) + + // time.Sleep(5 * time.Second) + // certsClient.RequestCertificates() + + // for { + // // try to renew certificate every day + // time.Sleep(24 * time.Hour) + // certsClient.RequestCertificates() + // } + // }() + + // go http.ServeHttp() + + // n.StartServer() + }, +} + +func Execute() { + command.Flags().Uint("dns-port", 53, "Port for the DNS server") + viper.BindPFlag("dns-port", command.Flags().Lookup("dns-port")) + + command.Flags().Uint("http-port", 80, "Port for the HTTP server") + viper.BindPFlag("http-port", command.Flags().Lookup("http-port")) + + command.Flags().Uint("https-port", 443, "Port for the HTTPS server") + viper.BindPFlag("https-port", command.Flags().Lookup("https-port")) + + command.Flags().Bool("staging", false, "Enable to use the Let's Encrypt staging environment to obtain certificates") + viper.BindPFlag("staging", command.Flags().Lookup("staging")) + + command.Flags().String("domain", "", "Root domain (required)") + command.MarkFlagRequired("domain") + viper.BindPFlag("domain", command.Flags().Lookup("domain")) + + command.Flags().String("email", "", "ACME account email address (required)") + command.MarkFlagRequired("email") + viper.BindPFlag("email", command.Flags().Lookup("email")) + + command.Flags().String("nameservers", "", "List of nameservers separated by commas") + command.MarkFlagRequired("nameservers") + + if err := command.Execute(); err != nil { + utils.Logger.Fatal().Err(err).Msg("") + } +} diff --git a/go.mod b/go.mod index ee68657..abb8468 100644 --- a/go.mod +++ b/go.mod @@ -6,18 +6,38 @@ require ( github.com/go-acme/lego/v4 v4.10.1 github.com/miekg/dns v1.1.57 github.com/rs/zerolog v1.33.0 - golang.org/x/net v0.17.0 + golang.org/x/net v0.23.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/crypto v0.14.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.13.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fc0a859..e9ffe18 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-acme/lego/v4 v4.10.1 h1:MiJvoBXNdmAwEK/SImyhwZ8ZL4IR0jtWDD1wST+N138= github.com/go-acme/lego/v4 v4.10.1/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= @@ -11,6 +15,12 @@ github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxF github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -19,25 +29,68 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -50,10 +103,14 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 9f4d601..38e41d6 100644 --- a/main.go +++ b/main.go @@ -1,35 +1,9 @@ package main import ( - "flag" - "time" - - "local-ip.sh/certs" - "local-ip.sh/http" - "local-ip.sh/xip" + "local-ip.sh/cmd" ) func main() { - port := flag.Int("port", 53, "port the DNS server should bind to") - flag.Parse() - - n := xip.NewXip(*port) - - go func() { - account := certs.LoadAccount() - certsClient := certs.NewCertsClient(n, account) - - time.Sleep(5 * time.Second) - certsClient.RequestCertificates() - - for { - // try to renew certificate every day - time.Sleep(24 * time.Hour) - certsClient.RequestCertificates() - } - }() - - go http.ServeHttp() - - n.StartServer() + cmd.Execute() } diff --git a/utils/config.go b/utils/config.go new file mode 100644 index 0000000..f2ff1cc --- /dev/null +++ b/utils/config.go @@ -0,0 +1,29 @@ +package utils + +import ( + "github.com/spf13/viper" +) + +type config struct { + DnsPort uint `mapstructure:"dns-port"` + HttpPort uint `mapstructure:"http-port"` + HttpsPort uint `mapstructure:"https-port"` + Domain string + Email string + + NameServers []string + CADirURL string + AccountFilePath string + KeyFilePath string +} + +var conf = &config{} + +func InitConfig() *config { + viper.Unmarshal(conf) + return conf +} + +func GetConfig() *config { + return conf +} diff --git a/xip/xip.go b/xip/xip.go index c9fae2f..3099f42 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -26,16 +26,12 @@ type HardcodedRecord struct { SRV *dns.SRV } -const ( - zone = "local-ip.sh." - nameservers = "ns1.local-ip.sh.,ns2.local-ip.sh." -) - var ( flyRegion = os.Getenv("FLY_REGION") dottedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})($|[.-])`) dashedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\-?\b){4})($|[.-])`) hardcodedRecords = map[string]HardcodedRecord{ + // TODO: maybe --nameservers ns1.local-ip.sh.=137.66.40.11,ns2.local-ip.sh.=137.66.40.12 "ns.local-ip.sh.": { // record holding ip addresses of ns1 and ns2 A: []net.IP{ @@ -424,18 +420,20 @@ func (xip *Xip) StartServer() { utils.Logger.Info().Str("dns_address", xip.server.Addr).Msg("DNS server listening") } -func NewXip(port int) (xip *Xip) { +func NewXip() (xip *Xip) { + config := utils.GetConfig() xip = &Xip{} - for _, ns := range strings.Split(nameservers, ",") { + for _, ns := range config.NameServers { xip.nameServers = append(xip.nameServers, &dns.NS{Ns: ns}) } xip.server = dns.Server{ - Addr: fmt.Sprintf(":%d", port), + Addr: fmt.Sprintf(":%d", config.DnsPort), Net: "udp", } + zone := fmt.Sprintf("%s.", config.Domain) dns.HandleFunc(zone, xip.handleDnsRequest) return xip -- 2.45.2 From e88cbff863bd177bc1e2cd39fb0b783c69deac69 Mon Sep 17 00:00:00 2001 From: m5r Date: Sun, 21 Jul 2024 23:30:59 +0200 Subject: [PATCH 02/11] move code around --- cmd/root.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 4845961..8fe5b27 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,17 +14,17 @@ import ( var command = &cobra.Command{ Use: "local-ip.sh", PreRun: func(cmd *cobra.Command, args []string) { - staging, err := cmd.Flags().GetBool("staging") - if err != nil { - utils.Logger.Fatal().Err(err).Msg("Unexpected error") - } - nameservers, err := cmd.Flags().GetString("nameservers") if err != nil { utils.Logger.Fatal().Err(err).Msg("Unexpected error") } viper.Set("NameServers", strings.Split(nameservers, ",")) + staging, err := cmd.Flags().GetBool("staging") + if err != nil { + utils.Logger.Fatal().Err(err).Msg("Unexpected error") + } + var caDir string if staging { caDir = lego.LEDirectoryStaging -- 2.45.2 From 92ad51d0018505d26b4eb05428faf2f9eeff7a29 Mon Sep 17 00:00:00 2001 From: m5r Date: Mon, 22 Jul 2024 01:06:47 +0200 Subject: [PATCH 03/11] extract hardcoded records --- xip/records.go | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ xip/xip.go | 138 +++++++++++--------------------------------- 2 files changed, 188 insertions(+), 103 deletions(-) create mode 100644 xip/records.go diff --git a/xip/records.go b/xip/records.go new file mode 100644 index 0000000..6fe7207 --- /dev/null +++ b/xip/records.go @@ -0,0 +1,153 @@ +package xip + +import ( + "fmt" + "net" + + "github.com/miekg/dns" + "local-ip.sh/utils" +) + +type hardcodedRecord struct { + A []net.IP // => dns.A + AAAA []net.IP // => dns.AAAA + TXT []string // => dns.TXT + MX []*dns.MX + CNAME []string // => dns.CNAME + SRV *dns.SRV +} + +var config = utils.GetConfig() +var hardcodedRecords = map[string]hardcodedRecord{ + // TODO: maybe --nameservers ns1.local-ip.sh.=137.66.40.11,ns2.local-ip.sh.=137.66.40.12 + fmt.Sprintf("ns.%s.", config.Domain): { + // record holding ip addresses of ns1 and ns2 + A: []net.IP{ + net.IPv4(137, 66, 40, 11), + net.IPv4(137, 66, 40, 12), + }, + }, + fmt.Sprintf("ns1.%s.", config.Domain): { + A: []net.IP{ + net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329 + }, + }, + fmt.Sprintf("ns2.%s.", config.Domain): { + A: []net.IP{ + net.IPv4(137, 66, 40, 12), // fly.io edge-only ip address #2 + }, + }, + fmt.Sprintf("%s.", config.Domain): { + // same as ns.local-ip.sh, it's the same machine :) + A: []net.IP{ + net.IPv4(137, 66, 40, 11), + net.IPv4(137, 66, 40, 12), + }, + }, + fmt.Sprintf("_acme-challenge.%s.", config.Domain): { + // will be filled in later when requesting the wildcard certificate + TXT: []string{}, + }, +} + +// additional records I set up to host emails, feel free to change or remove them for your own needs +var extraRecords = map[string]hardcodedRecord{ + "local-ip.sh.": { + TXT: []string{"v=spf1 include:capsulecorp.dev ~all"}, + MX: []*dns.MX{ + {Preference: 10, Mx: "email.capsulecorp.dev."}, + }, + }, + "autodiscover.local-ip.sh.": { + CNAME: []string{ + "email.capsulecorp.dev.", + }, + }, + "_autodiscover._tcp.local-ip.sh.": { + SRV: &dns.SRV{ + Priority: 0, + Weight: 0, + Port: 443, + Target: "email.capsulecorp.dev.", + }, + }, + "autoconfig.local-ip.sh.": { + CNAME: []string{ + "email.capsulecorp.dev.", + }, + }, + "_dmarc.local-ip.sh.": { + TXT: []string{"v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh"}, + }, + "dkim._domainkey.local-ip.sh.": { + TXT: []string{ + "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB", + }, + }, +} +var records = mergeRecords(hardcodedRecords, extraRecords) + +func mergeRecords(a, b map[string]hardcodedRecord) map[string]hardcodedRecord { + result := make(map[string]hardcodedRecord) + for k, v := range a { + result[k] = v + } + for k, v := range b { + if r, ok := result[k]; ok { + result[k] = hardcodedRecord{ + A: uniqueIPs(append(r.A, v.A...)), + AAAA: uniqueIPs(append(r.AAAA, v.AAAA...)), + TXT: uniqueStrings(append(r.TXT, v.TXT...)), + MX: uniqueMX(append(r.MX, v.MX...)), + CNAME: uniqueStrings(append(r.CNAME, v.CNAME...)), + SRV: firstNonNil(r.SRV, v.SRV), + } + } else { + result[k] = v + } + } + return result +} + +func uniqueIPs(ips []net.IP) []net.IP { + seen := make(map[string]bool) + result := []net.IP{} + for _, ip := range ips { + if !seen[ip.String()] { + seen[ip.String()] = true + result = append(result, ip) + } + } + return result +} + +func uniqueStrings(strs []string) []string { + seen := make(map[string]bool) + result := []string{} + for _, str := range strs { + if !seen[str] { + seen[str] = true + result = append(result, str) + } + } + return result +} + +func uniqueMX(mxs []*dns.MX) []*dns.MX { + seen := make(map[string]uint16) + result := []*dns.MX{} + for _, mx := range mxs { + if pref, exists := seen[mx.Mx]; !exists || pref > mx.Preference { + seen[mx.Mx] = mx.Preference + result = append(result, mx) + } + } + return result +} + +func firstNonNil[T any](a, b *T) *T { + if a != nil { + return a + } + return b +} diff --git a/xip/xip.go b/xip/xip.go index 3099f42..531ec64 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -17,113 +17,45 @@ type Xip struct { nameServers []*dns.NS } -type HardcodedRecord struct { - A []net.IP // => dns.A - AAAA []net.IP // => dns.AAAA - TXT []string // => dns.TXT - MX []*dns.MX - CNAME []string // => dns.CNAME - SRV *dns.SRV -} - var ( - flyRegion = os.Getenv("FLY_REGION") - dottedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})($|[.-])`) - dashedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\-?\b){4})($|[.-])`) - hardcodedRecords = map[string]HardcodedRecord{ - // TODO: maybe --nameservers ns1.local-ip.sh.=137.66.40.11,ns2.local-ip.sh.=137.66.40.12 - "ns.local-ip.sh.": { - // record holding ip addresses of ns1 and ns2 - A: []net.IP{ - net.IPv4(137, 66, 40, 11), - net.IPv4(137, 66, 40, 12), - }, - }, - "ns1.local-ip.sh.": { - A: []net.IP{ - net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329 - }, - }, - "ns2.local-ip.sh.": { - A: []net.IP{ - net.IPv4(137, 66, 40, 12), // fly.io edge-only ip address #2 - }, - }, - "local-ip.sh.": { - A: []net.IP{ - net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address - }, - TXT: []string{"v=spf1 include:capsulecorp.dev ~all"}, - MX: []*dns.MX{ - {Preference: 10, Mx: "email.capsulecorp.dev."}, - }, - }, - "autodiscover.local-ip.sh.": { - CNAME: []string{ - "email.capsulecorp.dev.", - }, - }, - "_autodiscover._tcp.local-ip.sh.": { - SRV: &dns.SRV{ - Priority: 0, - Weight: 0, - Port: 443, - Target: "email.capsulecorp.dev.", - }, - }, - "autoconfig.local-ip.sh.": { - CNAME: []string{ - "email.capsulecorp.dev.", - }, - }, - "_dmarc.local-ip.sh.": { - TXT: []string{"v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh"}, - }, - "dkim._domainkey.local-ip.sh.": { - TXT: []string{ - "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB", - }, - }, - "_acme-challenge.local-ip.sh.": { - // will be filled in later when requesting the wildcard certificate - TXT: []string{}, - }, - } + flyRegion = os.Getenv("FLY_REGION") + dottedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})($|[.-])`) + dashedIpV4Regex = regexp.MustCompile(`(?:^|(?:[\w\d])+\.)(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\-?\b){4})($|[.-])`) ) func (xip *Xip) SetTXTRecord(fqdn string, value string) { utils.Logger.Debug().Str("fqdn", fqdn).Str("value", value).Msg("Trying to set TXT record") - if fqdn != "_acme-challenge.local-ip.sh." { + if fqdn != fmt.Sprintf("_acme-challenge.%s.", config.Domain) { utils.Logger.Debug().Msg("Not allowed, abort") return } - if records, ok := hardcodedRecords[fqdn]; ok { - records.TXT = []string{value} - hardcodedRecords["_acme-challenge.local-ip.sh."] = records + if rootRecords, ok := records[fqdn]; ok { + rootRecords.TXT = []string{value} + records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords } } func (xip *Xip) UnsetTXTRecord(fqdn string) { utils.Logger.Debug().Str("fqdn", fqdn).Msg("Trying to set TXT record") - if fqdn != "_acme-challenge.local-ip.sh." { + if fqdn != fmt.Sprintf("_acme-challenge.%s.", config.Domain) { utils.Logger.Debug().Msg("Not allowed, abort") return } - if records, ok := hardcodedRecords[fqdn]; ok { - records.TXT = []string{} - hardcodedRecords["_acme-challenge.local-ip.sh."] = records + if rootRecords, ok := records[fqdn]; ok { + rootRecords.TXT = []string{} + records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords } } func (xip *Xip) fqdnToA(fqdn string) []*dns.A { normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].A != nil { - var records []*dns.A + if records[normalizedFqdn].A != nil { + var aRecords []*dns.A - for _, record := range hardcodedRecords[normalizedFqdn].A { - records = append(records, &dns.A{ + for _, record := range records[normalizedFqdn].A { + aRecords = append(aRecords, &dns.A{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), Name: fqdn, @@ -134,7 +66,7 @@ func (xip *Xip) fqdnToA(fqdn string) []*dns.A { }) } - return records + return aRecords } for _, ipV4RE := range []*regexp.Regexp{dashedIpV4Regex, dottedIpV4Regex} { @@ -167,15 +99,15 @@ func (xip *Xip) answerWithAuthority(question dns.Question, message *dns.Msg) { func (xip *Xip) handleA(question dns.Question, message *dns.Msg) { fqdn := question.Name - records := xip.fqdnToA(fqdn) + aRecords := xip.fqdnToA(fqdn) - if len(records) == 0 { + if len(aRecords) == 0 { message.Rcode = dns.RcodeNameError xip.answerWithAuthority(question, message) return } - for _, record := range records { + for _, record := range aRecords { message.Answer = append(message.Answer, record) } } @@ -183,12 +115,12 @@ func (xip *Xip) handleA(question dns.Question, message *dns.Msg) { func (xip *Xip) handleAAAA(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].AAAA == nil { + if records[normalizedFqdn].AAAA == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].AAAA { + for _, record := range records[normalizedFqdn].AAAA { message.Answer = append(message.Answer, &dns.AAAA{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -238,12 +170,12 @@ func chunkBy(str string, chunkSize int) (chunks []string) { func (xip *Xip) handleTXT(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].TXT == nil { + if records[normalizedFqdn].TXT == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].TXT { + for _, record := range records[normalizedFqdn].TXT { message.Answer = append(message.Answer, &dns.TXT{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -259,12 +191,12 @@ func (xip *Xip) handleTXT(question dns.Question, message *dns.Msg) { func (xip *Xip) handleMX(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].MX == nil { + if records[normalizedFqdn].MX == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].MX { + for _, record := range records[normalizedFqdn].MX { message.Answer = append(message.Answer, &dns.MX{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -281,12 +213,12 @@ func (xip *Xip) handleMX(question dns.Question, message *dns.Msg) { func (xip *Xip) handleCNAME(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].CNAME == nil { + if records[normalizedFqdn].CNAME == nil { xip.answerWithAuthority(question, message) return } - for _, record := range hardcodedRecords[normalizedFqdn].CNAME { + for _, record := range records[normalizedFqdn].CNAME { message.Answer = append(message.Answer, &dns.CNAME{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -302,7 +234,7 @@ func (xip *Xip) handleCNAME(question dns.Question, message *dns.Msg) { func (xip *Xip) handleSRV(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if hardcodedRecords[normalizedFqdn].SRV == nil { + if records[normalizedFqdn].SRV == nil { xip.answerWithAuthority(question, message) return } @@ -314,10 +246,10 @@ func (xip *Xip) handleSRV(question dns.Question, message *dns.Msg) { Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, - Priority: hardcodedRecords[normalizedFqdn].SRV.Priority, - Weight: hardcodedRecords[normalizedFqdn].SRV.Weight, - Port: hardcodedRecords[normalizedFqdn].SRV.Port, - Target: hardcodedRecords[normalizedFqdn].SRV.Target, + Priority: records[normalizedFqdn].SRV.Priority, + Weight: records[normalizedFqdn].SRV.Weight, + Port: records[normalizedFqdn].SRV.Port, + Target: records[normalizedFqdn].SRV.Target, }) } @@ -334,9 +266,9 @@ func (xip *Xip) soaRecord(question dns.Question) *dns.SOA { Ttl: uint32((time.Minute * 5).Seconds()), Rdlength: 0, } - soa.Ns = "ns1.local-ip.sh." - soa.Mbox = "admin.local-ip.sh." - soa.Serial = 2022102800 + soa.Ns = xip.nameServers[0].Ns + soa.Mbox = config.Email + soa.Serial = 2024072200 soa.Refresh = uint32((time.Minute * 15).Seconds()) soa.Retry = uint32((time.Minute * 15).Seconds()) soa.Expire = uint32((time.Minute * 30).Seconds()) -- 2.45.2 From 3a65254902b110070238349c867878e1ffdd1ce0 Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 00:56:24 +0200 Subject: [PATCH 04/11] env var working, got some validation done before running the cli --- Dockerfile | 15 +++---- cmd/root.go | 73 ++++++++++++++++++++-------------- compose.yml | 27 +++++++------ fly.toml | 9 ++--- go.mod | 5 ++- go.sum | 31 +++++++++------ http/server.go | 34 +++++++++++++--- xip/records.go | 105 +------------------------------------------------ xip/xip.go | 50 ++++++++++++++++++----- 9 files changed, 159 insertions(+), 190 deletions(-) diff --git a/Dockerfile b/Dockerfile index 994a62a..a319cab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,25 +4,20 @@ WORKDIR /app COPY . . RUN go mod download -RUN go build -o /app/local-ip +RUN go build FROM gcr.io/distroless/base-debian12:latest WORKDIR /local-ip -COPY --from=build /app/local-ip /local-ip/local-ip +COPY --from=build /app/local-ip.sh /local-ip/local-ip.sh COPY --from=build /app/http/static /local-ip/http/static VOLUME /local-ip/.lego -# DNS -EXPOSE 53/udp -# HTTP -EXPOSE 80/tcp -# HTTPS -EXPOSE 443/tcp +# DNS HTTP HTTPS +EXPOSE 53/udp 80/tcp 443/tcp USER root -# TODO: make these configurable too -CMD ["/local-ip/local-ip", "--domain", "local-ip.sh", "--email", "admin@local-ip.sh", "--nameservers", "ns1.local-ip.sh.,ns2.local-ip.sh."] +CMD ["/local-ip/local-ip.sh"] diff --git a/cmd/root.go b/cmd/root.go index 8fe5b27..e9521fe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,29 +2,48 @@ package cmd import ( "fmt" + "net/mail" "net/url" "strings" + "time" + "github.com/asaskevich/govalidator" "github.com/go-acme/lego/v4/lego" "github.com/spf13/cobra" "github.com/spf13/viper" + "local-ip.sh/certs" + "local-ip.sh/http" "local-ip.sh/utils" + "local-ip.sh/xip" ) var command = &cobra.Command{ Use: "local-ip.sh", PreRun: func(cmd *cobra.Command, args []string) { - nameservers, err := cmd.Flags().GetString("nameservers") - if err != nil { - utils.Logger.Fatal().Err(err).Msg("Unexpected error") - } - viper.Set("NameServers", strings.Split(nameservers, ",")) + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.SetEnvPrefix("XIP") + viper.AutomaticEnv() - staging, err := cmd.Flags().GetBool("staging") + email := viper.GetString("Email") + _, err := mail.ParseAddress(email) if err != nil { - utils.Logger.Fatal().Err(err).Msg("Unexpected error") + utils.Logger.Fatal().Err(err).Msg("Invalid email address") } + domain := viper.GetString("Domain") + if !govalidator.IsDNSName(domain) { + utils.Logger.Fatal().Err(err).Msg("Invalid domain") + } + + nameservers := strings.Split(viper.GetString("nameservers"), ",") + for _, ns := range nameservers { + if !govalidator.IsIPv4(ns) { + utils.Logger.Fatal().Err(err).Str("ns", ns).Msg("Invalid name server") + } + } + viper.Set("NameServers", nameservers) + + staging := viper.GetBool("staging") var caDir string if staging { caDir = lego.LEDirectoryStaging @@ -35,36 +54,32 @@ var command = &cobra.Command{ parsedCaDirUrl, _ := url.Parse(caDir) caDirHostname := parsedCaDirUrl.Hostname() - email := viper.GetString("Email") - viper.Set("AccountFilePath", fmt.Sprintf("./.lego/accounts/%s/%s/account.json", caDirHostname, email)) viper.Set("KeyFilePath", fmt.Sprintf("./.lego/accounts/%s/%s/keys/%s.key", caDirHostname, email, email)) utils.InitConfig() }, Run: func(cmd *cobra.Command, args []string) { - c := utils.GetConfig() - fmt.Printf("%#v\n", c) - fmt.Println(c) - // n := xip.NewXip() + n := xip.NewXip() - // go func() { - // account := certs.LoadAccount() - // certsClient := certs.NewCertsClient(n, account) + go func() { + // try to obtain certificates once the DNS server is accepting requests + account := certs.LoadAccount() + certsClient := certs.NewCertsClient(n, account) - // time.Sleep(5 * time.Second) - // certsClient.RequestCertificates() + time.Sleep(5 * time.Second) + certsClient.RequestCertificates() - // for { - // // try to renew certificate every day - // time.Sleep(24 * time.Hour) - // certsClient.RequestCertificates() - // } - // }() + for { + // afterwards, try to renew certificates once a day + time.Sleep(24 * time.Hour) + certsClient.RequestCertificates() + } + }() - // go http.ServeHttp() + go http.ServeHttp() - // n.StartServer() + n.StartServer() }, } @@ -82,15 +97,13 @@ func Execute() { viper.BindPFlag("staging", command.Flags().Lookup("staging")) command.Flags().String("domain", "", "Root domain (required)") - command.MarkFlagRequired("domain") viper.BindPFlag("domain", command.Flags().Lookup("domain")) command.Flags().String("email", "", "ACME account email address (required)") - command.MarkFlagRequired("email") viper.BindPFlag("email", command.Flags().Lookup("email")) - command.Flags().String("nameservers", "", "List of nameservers separated by commas") - command.MarkFlagRequired("nameservers") + command.Flags().String("nameservers", "", "List of nameservers separated by commas (required)") + viper.BindPFlag("nameservers", command.Flags().Lookup("nameservers")) if err := command.Execute(); err != nil { utils.Logger.Fatal().Err(err).Msg("") diff --git a/compose.yml b/compose.yml index b06b38c..3cdd327 100644 --- a/compose.yml +++ b/compose.yml @@ -1,14 +1,19 @@ services: - local-ip.sh: - image: local-ip.sh - build: . - volumes: - - lego:/local-ip/.lego - restart: unless-stopped - ports: - - 53:53/udp - - 80:80/tcp - - 443:443/tcp + local-ip.sh: + image: local-ip.sh + build: . + volumes: + - lego:/local-ip/.lego + restart: unless-stopped + environment: + XIP_DOMAIN: "local-ip.sh" + XIP_EMAIL: "admin@local-ip.sh" + XIP_NAMESERVERS: "137.66.40.11,137.66.40.12" + # XIP_STAGING: true + ports: + - 53:53/udp + - 80:80/tcp + - 443:443/tcp volumes: - lego: + lego: diff --git a/fly.toml b/fly.toml index 5b5bd1d..8772e76 100644 --- a/fly.toml +++ b/fly.toml @@ -1,8 +1,3 @@ -# fly.toml app configuration file generated for local-ip-ancient-glade-4376 on 2023-11-29T11:43:10+01:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - app = "local-ip" primary_region = "ams" kill_signal = "SIGINT" @@ -14,7 +9,9 @@ auto_rollback = true [build] [env] -PORT = "53" +XIP_DOMAIN = "local-ip.sh" +XIP_EMAIL = "admin@local-ip.sh" +XIP_NAMESERVERS = "137.66.40.11,137.66.40.12" # fly.io edge-only ip addresses, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329 [mounts] source = "lego" diff --git a/go.mod b/go.mod index abb8468..5dd1872 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module local-ip.sh go 1.22 require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/go-acme/lego/v4 v4.10.1 github.com/miekg/dns v1.1.57 github.com/rs/zerolog v1.33.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 golang.org/x/net v0.23.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -26,9 +29,7 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect diff --git a/go.sum b/go.sum index e9ffe18..43c304b 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-acme/lego/v4 v4.10.1 h1:MiJvoBXNdmAwEK/SImyhwZ8ZL4IR0jtWDD1wST+N138= @@ -13,12 +17,17 @@ github.com/go-acme/lego/v4 v4.10.1/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNyp github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -34,9 +43,11 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -65,8 +76,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -78,8 +87,6 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= @@ -87,12 +94,10 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqR golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -101,14 +106,14 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= diff --git a/http/server.go b/http/server.go index f69dcc3..826066d 100644 --- a/http/server.go +++ b/http/server.go @@ -2,8 +2,11 @@ package http import ( "context" + "fmt" + "net" "net/http" "os" + "strconv" "strings" "time" @@ -45,8 +48,9 @@ func registerHandlers() { } func serveHttp() *http.Server { - utils.Logger.Info().Msg("Starting up HTTP server on :80") - httpServer := &http.Server{Addr: ":http"} + config := utils.GetConfig() + httpServer := &http.Server{Addr: fmt.Sprintf(":%d", config.HttpPort)} + utils.Logger.Info().Str("http_address", httpServer.Addr).Msg("Starting up HTTP server") go func() { err := httpServer.ListenAndServe() if err != http.ErrServerClosed { @@ -84,22 +88,40 @@ func killServer(httpServer *http.Server) { } func redirectHttpToHttps() { - utils.Logger.Info().Msg("Redirecting HTTP traffic from :80 to HTTPS :443") + config := utils.GetConfig() httpServer := &http.Server{ - Addr: ":http", + Addr: fmt.Sprintf(":%d", config.HttpPort), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { url := r.URL + host := r.Host + + // Strip the port from the host if present + if strings.Contains(host, ":") { + hostWithoutPort, _, err := net.SplitHostPort(host) + if err != nil { + utils.Logger.Error().Err(err).Msg("Failed to split host and port") + } else { + host = hostWithoutPort + } + } + // Add the HTTPS port only if it's not 443 + if config.HttpsPort != 443 { + host = net.JoinHostPort(host, strconv.FormatUint(uint64(config.HttpsPort), 10)) + } + url.Host = r.Host url.Scheme = "https" http.Redirect(w, r, url.String(), http.StatusMovedPermanently) }), } + utils.Logger.Info().Str("http_address", httpServer.Addr).Msg("Redirecting HTTP traffic to HTTPS") go httpServer.ListenAndServe() } func serveHttps() { - utils.Logger.Info().Msg("Starting up HTTPS server on :443") - httpsServer := &http.Server{Addr: ":https"} + config := utils.GetConfig() + httpsServer := &http.Server{Addr: fmt.Sprintf(":%d", config.HttpsPort)} + utils.Logger.Info().Str("https_address", httpsServer.Addr).Msg("Starting up HTTPS server") go httpsServer.ListenAndServeTLS("./.lego/certs/root/server.pem", "./.lego/certs/root/server.key") } diff --git a/xip/records.go b/xip/records.go index 6fe7207..13fce73 100644 --- a/xip/records.go +++ b/xip/records.go @@ -1,11 +1,9 @@ package xip import ( - "fmt" "net" "github.com/miekg/dns" - "local-ip.sh/utils" ) type hardcodedRecord struct { @@ -17,41 +15,8 @@ type hardcodedRecord struct { SRV *dns.SRV } -var config = utils.GetConfig() -var hardcodedRecords = map[string]hardcodedRecord{ - // TODO: maybe --nameservers ns1.local-ip.sh.=137.66.40.11,ns2.local-ip.sh.=137.66.40.12 - fmt.Sprintf("ns.%s.", config.Domain): { - // record holding ip addresses of ns1 and ns2 - A: []net.IP{ - net.IPv4(137, 66, 40, 11), - net.IPv4(137, 66, 40, 12), - }, - }, - fmt.Sprintf("ns1.%s.", config.Domain): { - A: []net.IP{ - net.IPv4(137, 66, 40, 11), // fly.io edge-only ip address, see https://community.fly.io/t/custom-domains-certificate-is-stuck-on-awaiting-configuration/8329 - }, - }, - fmt.Sprintf("ns2.%s.", config.Domain): { - A: []net.IP{ - net.IPv4(137, 66, 40, 12), // fly.io edge-only ip address #2 - }, - }, - fmt.Sprintf("%s.", config.Domain): { - // same as ns.local-ip.sh, it's the same machine :) - A: []net.IP{ - net.IPv4(137, 66, 40, 11), - net.IPv4(137, 66, 40, 12), - }, - }, - fmt.Sprintf("_acme-challenge.%s.", config.Domain): { - // will be filled in later when requesting the wildcard certificate - TXT: []string{}, - }, -} - -// additional records I set up to host emails, feel free to change or remove them for your own needs -var extraRecords = map[string]hardcodedRecord{ +var records = map[string]hardcodedRecord{ + // additional records I set up to host emails, feel free to change or remove them for your own needs "local-ip.sh.": { TXT: []string{"v=spf1 include:capsulecorp.dev ~all"}, MX: []*dns.MX{ @@ -85,69 +50,3 @@ var extraRecords = map[string]hardcodedRecord{ }, }, } -var records = mergeRecords(hardcodedRecords, extraRecords) - -func mergeRecords(a, b map[string]hardcodedRecord) map[string]hardcodedRecord { - result := make(map[string]hardcodedRecord) - for k, v := range a { - result[k] = v - } - for k, v := range b { - if r, ok := result[k]; ok { - result[k] = hardcodedRecord{ - A: uniqueIPs(append(r.A, v.A...)), - AAAA: uniqueIPs(append(r.AAAA, v.AAAA...)), - TXT: uniqueStrings(append(r.TXT, v.TXT...)), - MX: uniqueMX(append(r.MX, v.MX...)), - CNAME: uniqueStrings(append(r.CNAME, v.CNAME...)), - SRV: firstNonNil(r.SRV, v.SRV), - } - } else { - result[k] = v - } - } - return result -} - -func uniqueIPs(ips []net.IP) []net.IP { - seen := make(map[string]bool) - result := []net.IP{} - for _, ip := range ips { - if !seen[ip.String()] { - seen[ip.String()] = true - result = append(result, ip) - } - } - return result -} - -func uniqueStrings(strs []string) []string { - seen := make(map[string]bool) - result := []string{} - for _, str := range strs { - if !seen[str] { - seen[str] = true - result = append(result, str) - } - } - return result -} - -func uniqueMX(mxs []*dns.MX) []*dns.MX { - seen := make(map[string]uint16) - result := []*dns.MX{} - for _, mx := range mxs { - if pref, exists := seen[mx.Mx]; !exists || pref > mx.Preference { - seen[mx.Mx] = mx.Preference - result = append(result, mx) - } - } - return result -} - -func firstNonNil[T any](a, b *T) *T { - if a != nil { - return a - } - return b -} diff --git a/xip/xip.go b/xip/xip.go index 531ec64..87141f8 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -14,7 +14,7 @@ import ( type Xip struct { server dns.Server - nameServers []*dns.NS + nameServers []string } var ( @@ -25,6 +25,7 @@ var ( func (xip *Xip) SetTXTRecord(fqdn string, value string) { utils.Logger.Debug().Str("fqdn", fqdn).Str("value", value).Msg("Trying to set TXT record") + config := utils.GetConfig() if fqdn != fmt.Sprintf("_acme-challenge.%s.", config.Domain) { utils.Logger.Debug().Msg("Not allowed, abort") return @@ -38,6 +39,7 @@ func (xip *Xip) SetTXTRecord(fqdn string, value string) { func (xip *Xip) UnsetTXTRecord(fqdn string) { utils.Logger.Debug().Str("fqdn", fqdn).Msg("Trying to set TXT record") + config := utils.GetConfig() if fqdn != fmt.Sprintf("_acme-challenge.%s.", config.Domain) { utils.Logger.Debug().Msg("Not allowed, abort") return @@ -145,10 +147,10 @@ func (xip *Xip) handleNS(question dns.Question, message *dns.Msg) { Rrtype: dns.TypeNS, Class: dns.ClassINET, }, - Ns: ns.Ns, + Ns: ns, }) - additionals = append(additionals, xip.fqdnToA(ns.Ns)...) + additionals = append(additionals, xip.fqdnToA(ns)...) } for _, record := range nameServers { @@ -257,7 +259,15 @@ func (xip *Xip) handleSOA(question dns.Question, message *dns.Msg) { message.Answer = append(message.Answer, xip.soaRecord(question)) } +func emailToRname(email string) string { + parts := strings.SplitN(email, "@", 2) + localPart := strings.ReplaceAll(parts[0], ".", "\\.") + domain := parts[1] + return localPart + "." + domain + "." +} + func (xip *Xip) soaRecord(question dns.Question) *dns.SOA { + config := utils.GetConfig() soa := new(dns.SOA) soa.Hdr = dns.RR_Header{ Name: question.Name, @@ -266,8 +276,8 @@ func (xip *Xip) soaRecord(question dns.Question) *dns.SOA { Ttl: uint32((time.Minute * 5).Seconds()), Rdlength: 0, } - soa.Ns = xip.nameServers[0].Ns - soa.Mbox = config.Email + soa.Ns = xip.nameServers[0] + soa.Mbox = emailToRname(config.Email) soa.Serial = 2024072200 soa.Refresh = uint32((time.Minute * 15).Seconds()) soa.Retry = uint32((time.Minute * 15).Seconds()) @@ -321,6 +331,7 @@ func (xip *Xip) handleDnsRequest(response dns.ResponseWriter, request *dns.Msg) error := response.WriteMsg(message) if error != nil { + utils.Logger.Debug().Msg(message.String()) utils.Logger.Error().Err(error).Str("message", message.String()).Msg("Error responding to query") } }() @@ -335,6 +346,7 @@ func (xip *Xip) StartServer() { err := xip.server.ListenAndServe() defer xip.server.Shutdown() if err != nil { + utils.Logger.Fatal().Err(err).Msg("Failed to start DNS server") if strings.Contains(err.Error(), "fly-global-services: no such host") { // we're not running on fly, bind to 0.0.0.0 instead port := strings.Split(xip.server.Addr, ":")[1] @@ -349,16 +361,36 @@ func (xip *Xip) StartServer() { utils.Logger.Fatal().Err(err).Msg("Failed to start DNS server") } - utils.Logger.Info().Str("dns_address", xip.server.Addr).Msg("DNS server listening") + utils.Logger.Info().Str("dns_address", xip.server.Addr).Msg("Starting up DNS server") +} + +func (xip *Xip) initHardcodedRecords() { + config := utils.GetConfig() + rootDomainARecords := []net.IP{} + + for i, ns := range config.NameServers { + name := fmt.Sprintf("ns%d.%s.", i+1, config.Domain) + ip := net.ParseIP(ns) + + rootDomainARecords = append(rootDomainARecords, ip) + entry := records[name] + entry.A = append(records[name].A, ip) + records[name] = entry + + xip.nameServers = append(xip.nameServers, name) + } + + records[fmt.Sprintf("%s.", config.Domain)] = hardcodedRecord{A: rootDomainARecords} + + // will be filled in later when requesting the wildcard certificate + records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = hardcodedRecord{TXT: []string{}} } func NewXip() (xip *Xip) { config := utils.GetConfig() xip = &Xip{} - for _, ns := range config.NameServers { - xip.nameServers = append(xip.nameServers, &dns.NS{Ns: ns}) - } + xip.initHardcodedRecords() xip.server = dns.Server{ Addr: fmt.Sprintf(":%d", config.DnsPort), -- 2.45.2 From 7ade2a9afea1cc4296e4ec3dea06357bfb28dc9e Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:03:03 +0200 Subject: [PATCH 05/11] document how to configure --- README.md | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 6c12e98..be40ef3 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,21 @@ [local-ip.sh](https://local-ip.sh) is a magic domain name that provides wildcard DNS for any IP address. It is heavily inspired by [local-ip.co](http://local-ip.co), [sslip.io](https://sslip.io), and [xip.io](https://xip.io) +## How it works + +local-ip.sh packs up: + - an authoritative DNS server that answers queries for the zone `local-ip.sh` + - a Let's Encrypt client that takes care of obtaining and renewing the wildcard certificate for `*.local-ip.sh` and the root certificate for `local-ip.sh` using the [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) + - an HTTP server that serves the website and the wildcard certificate files + +It answers queries with the IPv4 address it may find in the subdomain by pattern matching the FQDN. +It registers an account to Let's Encrypt's ACME server to obtain the wildcard certificate on the first run and then renew it about a month before it expires. The account file and the associated key used to request a certificate under the `./.lego/accounts` directory and the certificate's files are stored in `./.lego/certs`. +It also obtains a separate certificate for the root domain to serve the website through HTTPS. It initially serves the website through HTTP and when the root domain certificate is ready, it redirects all HTTP requests to HTTPS. + ## Usage ```sh -go run ./main.go # binds to :53 by default but you can override it by using the `-port` parameter +go run ./main.go --staging --dns-port 9053 --http-port 9080 --https-port 9443 --domain local-ip.sh --email admin@fake.sh --nameservers 137.66.40.11,137.66.40.12 dig @localhost 10-0-1-29.my.local-ip.sh +short # 10.0.1.29 @@ -18,33 +29,25 @@ dig @localhost 127.0.0.1.my.local-ip.sh +short # 127.0.0.1 ``` -## How it works +### Configuration -local-ip.sh packs up: - - an authoritative DNS server that answers queries for the zone `local-ip.sh` - - a Let's Encrypt client that takes care of obtaining and renewing the wildcard certificate for `*.local-ip.sh` and the root certificate for `local-ip.sh` using the [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) - - an HTTP server that serves static files, including the certificate files +local-ip.sh can be configured through environment variables or CLI flags -It answers queries with the IPv4 address it may find in the subdomain by pattern matching the FQDN. -It registers an account to Let's Encrypt's ACME server to obtain the wildcard certificate on the first run and then renew -it about a month before it expires. The account file and the associated key used to request a certificate under the `./.lego/accounts` -directory and the certificate's files are stored in `./.lego/certs`. +- `XIP_DNS_PORT` or `--dns-port` optional, port for the DNS server, defaults to `53`. +- `XIP_HTTP_PORT` or `--http-port` optional, port for the HTTP server, defaults to `80`. +- `XIP_HTTPS_PORT` or `--https-port` optional, port for the HTTPS server, defaults to `443`. +- `XIP_STAGING` or `--staging` optional, enable to use Let's Encrypt staging environment to obtain certificates, defaults to `false`. +- `XIP_DOMAIN` or `--domain` required, domain name of the server hosting this. It will be used as the zone to answer dns queries for. +- `XIP_EMAIL` or `--email` required, administrator's email address, used to create the ACME account to request certificates from Let's Encrypt and as the `RNAME` value of the SOA record representing the domain administrator's email address. +- `XIP_NAMESERVERS` or `--nameservers` required, comma-separated IPv4 addresses used to answer `A` queries for `nsX.{domain}` where `X` is the index of the address in this list. For example setting `--domain example.com --nameservers 1.2.3.4,9.8.7.6` will answer `1.2.3.4` for `ns1.example.com` and `9.8.7.6` for `ns2.example.com`. All `nsX.{domain}` nameservers will be in the answer for NS queries to the zone. -The certificate files are served by an HTTP server on the arbitrary port `:9229` that is intentionally not exposed to -the internet. [The website](https://local-ip.sh) is connected to the same private network as the service and serves -as a proxy to access the files securely. +A [reference docker compose file](./compose.yml) is available ## Self-hosting -I'm currently hosting [local-ip.sh](https://local-ip.sh) at [Fly.io](https://fly.io) but you can host the service yourself -if you're into that kind of thing. Note that you will need to edit your domain's glue records so make sure your registrar allows it. +I'm currently hosting [local-ip.sh](https://local-ip.sh) at [Fly.io](https://fly.io) but you can host the service yourself if you're into that kind of thing. Note that you will need to edit your domain's glue records so make sure your registrar allows it. You will essentially need to: - - replace any occurrence of `local-ip.sh` in `.go` files with your domain - - replace the hardcoded IP addresses in the `hardcodedRecords` map declared in [`xip.go:37`](./xip/xip.go#L37), the important records to keep are: - - `A ns.local-ip.sh.` holds both IP addresses pointing to `ns1.` and `ns2.` - - `A ns1.local-ip.sh.` holds the first IP address pointing to the server hosting local-ip.sh - - `A ns2.local-ip.sh.` holds the second IP address pointing to the server, exists for redundancy - - `TXT _acme-challenge.local-ip.sh.` will temporarily hold the value to solve the DNS-01 challenge - - set your domain's glue records to point to the IP addresses you set for `ns1.` and `ns2.` - - retrieve the certificate files once the program is up and running + - set your domain's glue records to point to the IP addresses you will set for `XIP_NAMESERVERS` / `--nameservers` + - configure `local-ip.sh` with the domain, admin email address, and nameservers + - ensure you have some sort of persistent storage for the `./.lego` directory, this is where the ACME account and certificate files are stored -- 2.45.2 From ee62998a0c15bae32fd9da520ac1d010f3dc633c Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:04:15 +0200 Subject: [PATCH 06/11] fix usage example --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index be40ef3..e8e0dc8 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ It also obtains a separate certificate for the root domain to serve the website ```sh go run ./main.go --staging --dns-port 9053 --http-port 9080 --https-port 9443 --domain local-ip.sh --email admin@fake.sh --nameservers 137.66.40.11,137.66.40.12 -dig @localhost 10-0-1-29.my.local-ip.sh +short +dig @localhost -p 9053 10-0-1-29.local-ip.sh +short # 10.0.1.29 -dig @localhost app.10-0-1-29.my.local-ip.sh +short +dig @localhost -p 9053 app.10-0-1-29.local-ip.sh +short # 10.0.1.29 -dig @localhost foo.bar.10.0.1.29.my.local-ip.sh +short +dig @localhost -p 9053 foo.bar.10.0.1.29.local-ip.sh +short # 10.0.1.29 -dig @localhost 127.0.0.1.my.local-ip.sh +short +dig @localhost -p 9053 127.0.0.1.local-ip.sh +short # 127.0.0.1 ``` -- 2.45.2 From ba93a1392358cb841b39ffb255dcd2628b6cc60e Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:04:59 +0200 Subject: [PATCH 07/11] finish sentence --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8e0dc8..f376127 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ local-ip.sh can be configured through environment variables or CLI flags - `XIP_EMAIL` or `--email` required, administrator's email address, used to create the ACME account to request certificates from Let's Encrypt and as the `RNAME` value of the SOA record representing the domain administrator's email address. - `XIP_NAMESERVERS` or `--nameservers` required, comma-separated IPv4 addresses used to answer `A` queries for `nsX.{domain}` where `X` is the index of the address in this list. For example setting `--domain example.com --nameservers 1.2.3.4,9.8.7.6` will answer `1.2.3.4` for `ns1.example.com` and `9.8.7.6` for `ns2.example.com`. All `nsX.{domain}` nameservers will be in the answer for NS queries to the zone. -A [reference docker compose file](./compose.yml) is available +A [reference docker compose file](./compose.yml) is available for deployments using Docker. ## Self-hosting -- 2.45.2 From fecb0c2d503134df470de61709e77ede6e0d674a Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:05:58 +0200 Subject: [PATCH 08/11] finish sentence --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f376127..34c9265 100644 --- a/README.md +++ b/README.md @@ -50,4 +50,4 @@ I'm currently hosting [local-ip.sh](https://local-ip.sh) at [Fly.io](https://fly You will essentially need to: - set your domain's glue records to point to the IP addresses you will set for `XIP_NAMESERVERS` / `--nameservers` - configure `local-ip.sh` with the domain, admin email address, and nameservers - - ensure you have some sort of persistent storage for the `./.lego` directory, this is where the ACME account and certificate files are stored + - ensure you have some sort of persistent storage for the `./.lego` directory, this is where the ACME account and certificate files are stored, you don't want to lose this between deployments -- 2.45.2 From 26a0d968a7b62f17ab847f427a3ae5878c4c1d22 Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:07:03 +0200 Subject: [PATCH 09/11] fill message --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index e9521fe..e0e8af2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -106,6 +106,6 @@ func Execute() { viper.BindPFlag("nameservers", command.Flags().Lookup("nameservers")) if err := command.Execute(); err != nil { - utils.Logger.Fatal().Err(err).Msg("") + utils.Logger.Fatal().Err(err).Msg("Failed to run local-ip.sh") } } -- 2.45.2 From 8ee3d3ce75fe50a66dcf8bda077406813b8f077a Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:10:06 +0200 Subject: [PATCH 10/11] s/records/hardcodedrecords --- xip/records.go | 2 +- xip/xip.go | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/xip/records.go b/xip/records.go index 13fce73..4da457a 100644 --- a/xip/records.go +++ b/xip/records.go @@ -15,7 +15,7 @@ type hardcodedRecord struct { SRV *dns.SRV } -var records = map[string]hardcodedRecord{ +var hardcodedRecords = map[string]hardcodedRecord{ // additional records I set up to host emails, feel free to change or remove them for your own needs "local-ip.sh.": { TXT: []string{"v=spf1 include:capsulecorp.dev ~all"}, diff --git a/xip/xip.go b/xip/xip.go index 87141f8..d1f1c0c 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -31,9 +31,9 @@ func (xip *Xip) SetTXTRecord(fqdn string, value string) { return } - if rootRecords, ok := records[fqdn]; ok { + if rootRecords, ok := hardcodedRecords[fqdn]; ok { rootRecords.TXT = []string{value} - records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords + hardcodedRecords[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords } } @@ -45,18 +45,18 @@ func (xip *Xip) UnsetTXTRecord(fqdn string) { return } - if rootRecords, ok := records[fqdn]; ok { + if rootRecords, ok := hardcodedRecords[fqdn]; ok { rootRecords.TXT = []string{} - records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords + hardcodedRecords[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = rootRecords } } func (xip *Xip) fqdnToA(fqdn string) []*dns.A { normalizedFqdn := strings.ToLower(fqdn) - if records[normalizedFqdn].A != nil { + if hardcodedRecords[normalizedFqdn].A != nil { var aRecords []*dns.A - for _, record := range records[normalizedFqdn].A { + for _, record := range hardcodedRecords[normalizedFqdn].A { aRecords = append(aRecords, &dns.A{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -117,12 +117,12 @@ func (xip *Xip) handleA(question dns.Question, message *dns.Msg) { func (xip *Xip) handleAAAA(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if records[normalizedFqdn].AAAA == nil { + if hardcodedRecords[normalizedFqdn].AAAA == nil { xip.answerWithAuthority(question, message) return } - for _, record := range records[normalizedFqdn].AAAA { + for _, record := range hardcodedRecords[normalizedFqdn].AAAA { message.Answer = append(message.Answer, &dns.AAAA{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -172,12 +172,12 @@ func chunkBy(str string, chunkSize int) (chunks []string) { func (xip *Xip) handleTXT(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if records[normalizedFqdn].TXT == nil { + if hardcodedRecords[normalizedFqdn].TXT == nil { xip.answerWithAuthority(question, message) return } - for _, record := range records[normalizedFqdn].TXT { + for _, record := range hardcodedRecords[normalizedFqdn].TXT { message.Answer = append(message.Answer, &dns.TXT{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -193,12 +193,12 @@ func (xip *Xip) handleTXT(question dns.Question, message *dns.Msg) { func (xip *Xip) handleMX(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if records[normalizedFqdn].MX == nil { + if hardcodedRecords[normalizedFqdn].MX == nil { xip.answerWithAuthority(question, message) return } - for _, record := range records[normalizedFqdn].MX { + for _, record := range hardcodedRecords[normalizedFqdn].MX { message.Answer = append(message.Answer, &dns.MX{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -215,12 +215,12 @@ func (xip *Xip) handleMX(question dns.Question, message *dns.Msg) { func (xip *Xip) handleCNAME(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if records[normalizedFqdn].CNAME == nil { + if hardcodedRecords[normalizedFqdn].CNAME == nil { xip.answerWithAuthority(question, message) return } - for _, record := range records[normalizedFqdn].CNAME { + for _, record := range hardcodedRecords[normalizedFqdn].CNAME { message.Answer = append(message.Answer, &dns.CNAME{ Hdr: dns.RR_Header{ Ttl: uint32((time.Minute * 5).Seconds()), @@ -236,7 +236,7 @@ func (xip *Xip) handleCNAME(question dns.Question, message *dns.Msg) { func (xip *Xip) handleSRV(question dns.Question, message *dns.Msg) { fqdn := question.Name normalizedFqdn := strings.ToLower(fqdn) - if records[normalizedFqdn].SRV == nil { + if hardcodedRecords[normalizedFqdn].SRV == nil { xip.answerWithAuthority(question, message) return } @@ -248,10 +248,10 @@ func (xip *Xip) handleSRV(question dns.Question, message *dns.Msg) { Rrtype: dns.TypeSRV, Class: dns.ClassINET, }, - Priority: records[normalizedFqdn].SRV.Priority, - Weight: records[normalizedFqdn].SRV.Weight, - Port: records[normalizedFqdn].SRV.Port, - Target: records[normalizedFqdn].SRV.Target, + Priority: hardcodedRecords[normalizedFqdn].SRV.Priority, + Weight: hardcodedRecords[normalizedFqdn].SRV.Weight, + Port: hardcodedRecords[normalizedFqdn].SRV.Port, + Target: hardcodedRecords[normalizedFqdn].SRV.Target, }) } @@ -373,17 +373,17 @@ func (xip *Xip) initHardcodedRecords() { ip := net.ParseIP(ns) rootDomainARecords = append(rootDomainARecords, ip) - entry := records[name] - entry.A = append(records[name].A, ip) - records[name] = entry + entry := hardcodedRecords[name] + entry.A = append(hardcodedRecords[name].A, ip) + hardcodedRecords[name] = entry xip.nameServers = append(xip.nameServers, name) } - records[fmt.Sprintf("%s.", config.Domain)] = hardcodedRecord{A: rootDomainARecords} + hardcodedRecords[fmt.Sprintf("%s.", config.Domain)] = hardcodedRecord{A: rootDomainARecords} // will be filled in later when requesting the wildcard certificate - records[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = hardcodedRecord{TXT: []string{}} + hardcodedRecords[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = hardcodedRecord{TXT: []string{}} } func NewXip() (xip *Xip) { -- 2.45.2 From 263d5b3a90b6fa838384e67e791c7a61d0dbd8e1 Mon Sep 17 00:00:00 2001 From: m5r Date: Fri, 26 Jul 2024 12:11:44 +0200 Subject: [PATCH 11/11] bump SOA serial --- xip/xip.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xip/xip.go b/xip/xip.go index d1f1c0c..cea8323 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -278,7 +278,7 @@ func (xip *Xip) soaRecord(question dns.Question) *dns.SOA { } soa.Ns = xip.nameServers[0] soa.Mbox = emailToRname(config.Email) - soa.Serial = 2024072200 + soa.Serial = 2024072600 soa.Refresh = uint32((time.Minute * 15).Seconds()) soa.Retry = uint32((time.Minute * 15).Seconds()) soa.Expire = uint32((time.Minute * 30).Seconds()) @@ -382,7 +382,7 @@ func (xip *Xip) initHardcodedRecords() { hardcodedRecords[fmt.Sprintf("%s.", config.Domain)] = hardcodedRecord{A: rootDomainARecords} - // will be filled in later when requesting the wildcard certificate + // will be filled in later when requesting certificates hardcodedRecords[fmt.Sprintf("_acme-challenge.%s.", config.Domain)] = hardcodedRecord{TXT: []string{}} } -- 2.45.2