Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<p align="center" width="50">

[![GitHub Repo stars](https://img.shields.io/github/stars/lachlanharrisdev/gonetsim?style=social)](https://github.com/lachlanharrisdev/gonetsim/stargazers)
[![GitHub](https://img.shields.io/github/license/lachlanharrisdev/gonetsim)](https://github.com/lachlanharrisdev/gonetsim/?tab=Apache-2.0-1-ov-file)
[![GitHub](https://img.shields.io/github/license/lachlanharrisdev/gonetsim)](https://github.com/lachlanharrisdev/gonetsim?tab=Apache-2.0-1-ov-file)
[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/lachlanharrisdev/gonetsim)](https://github.com/lachlanharrisdev/gonetsim/)
[![GitHub CI Status](https://img.shields.io/github/actions/workflow/status/lachlanharrisdev/gonetsim/ci.yaml?branch=main&label=CI)](https://github.com/lachlanharrisdev/gonetsim/actions)<br/>
[![GitHub Release Status](https://img.shields.io/github/v/release/lachlanharrisdev/gonetsim)](https://github.com/lachlanharrisdev/gonetsim/releases/latest)
Expand Down Expand Up @@ -50,6 +50,9 @@ Alternatively, you can specify an individual service to run
gonetsim dns
gonetsim http
gonetsim https
gonetsim smtp
gonetsim smtps
...
```

<br/>
Expand Down
40 changes: 38 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/lachlanharrisdev/gonetsim/internal/httpserver"
"github.com/lachlanharrisdev/gonetsim/internal/observability"
"github.com/lachlanharrisdev/gonetsim/internal/service"
"github.com/lachlanharrisdev/gonetsim/internal/smtpserver"
"github.com/lachlanharrisdev/gonetsim/internal/utils"
"github.com/spf13/cobra"
)
Expand All @@ -19,7 +20,7 @@ var rootConfigPath string

var rootCmd = &cobra.Command{
Use: "gonetsim",
Short: "Network service simulator (dns + http + https)",
Short: "Starts all configured services",
Args: cobra.NoArgs,
SilenceUsage: true,
SilenceErrors: true,
Expand Down Expand Up @@ -93,7 +94,42 @@ var rootCmd = &cobra.Command{
manager.Add(httpserver.NewHTTPSService(conf, tlsOpts))
}

logger.Info("running", "dns", cfg.DNS.Enabled, "http", cfg.HTTP.Enabled, "https", cfg.HTTPS.Enabled)
if cfg.SMTP.Enabled {
listen, err := parseAddrPort(cfg.SMTP.Addr)
if err != nil {
return fmt.Errorf("smtp.addr: %w", err)
}
conf := smtpserver.Config{
Addr: listen,
Domain: cfg.SMTP.Domain,
WriteTimeout: cfg.SMTP.WriteTimeout,
ReadTimeout: cfg.SMTP.ReadTimeout,
MaxMessageBytes: cfg.SMTP.MaxMessageBytes,
MaxRecipients: cfg.SMTP.MaxRecipients,
AllowInsecureAuth: cfg.SMTP.AllowInsecureAuth,
}
manager.Add(smtpserver.NewSMTPService(conf))
}

if cfg.SMTPS.Enabled {
listen, err := parseAddrPort(cfg.SMTPS.Addr)
if err != nil {
return fmt.Errorf("smtps.addr: %w", err)
}
conf := smtpserver.Config{
Addr: listen,
Domain: cfg.SMTPS.Domain,
WriteTimeout: cfg.SMTPS.WriteTimeout,
ReadTimeout: cfg.SMTPS.ReadTimeout,
MaxMessageBytes: cfg.SMTPS.MaxMessageBytes,
MaxRecipients: cfg.SMTPS.MaxRecipients,
AllowInsecureAuth: cfg.SMTPS.AllowInsecureAuth,
}
tlsOpts := smtpserver.TLSOptions{CertFile: cfg.SMTPS.Cert, KeyFile: cfg.SMTPS.Key}
manager.Add(smtpserver.NewSMTPSService(conf, tlsOpts))
}

logger.Info("running", "dns", cfg.DNS.Enabled, "http", cfg.HTTP.Enabled, "https", cfg.HTTPS.Enabled, "smtp", cfg.SMTP.Enabled, "smtps", cfg.SMTPS.Enabled)

return manager.RunAll(runCtx)
},
Expand Down
71 changes: 71 additions & 0 deletions cmd/smtp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cmd

import (
"context"
"log/slog"
"time"

appconfig "github.com/lachlanharrisdev/gonetsim/internal/config"
"github.com/lachlanharrisdev/gonetsim/internal/observability"
"github.com/lachlanharrisdev/gonetsim/internal/service"
"github.com/lachlanharrisdev/gonetsim/internal/smtpserver"
"github.com/lachlanharrisdev/gonetsim/internal/utils"
"github.com/spf13/cobra"
)

var (
smtpAddr string
smtpDomain string
smtpWriteTimeout int
smtpReadTimeout int
smtpMaxMessageBytes int
smtpMaxRecipients int
smtpAllowInsecureAuth bool
)

var smtpCmd = &cobra.Command{
Use: "smtp",
Short: "Run an SMTP server (insecure, no TLS)",
RunE: func(cmd *cobra.Command, args []string) error {
listen, err := parseAddrPort(smtpAddr)
if err != nil {
return err
}

ctx, stop := utils.SignalContext(context.Background())
defer stop()

logger, err := observability.NewLogger(appconfig.Default().Logging)
if err != nil {
return err
}
slog.SetDefault(logger)
manager := service.NewManager(5*time.Second, logger)

return manager.RunSingleService(ctx,
smtpserver.NewSMTPService(
smtpserver.Config{
Addr: listen,
Domain: smtpDomain,
WriteTimeout: smtpWriteTimeout,
ReadTimeout: smtpReadTimeout,
MaxMessageBytes: smtpMaxMessageBytes,
MaxRecipients: smtpMaxRecipients,
AllowInsecureAuth: smtpAllowInsecureAuth,
},
),
)
},
}

func init() {
rootCmd.AddCommand(smtpCmd)

smtpCmd.Flags().StringVar(&smtpAddr, "listen", ":1025", "listen address")
smtpCmd.Flags().StringVar(&smtpDomain, "domain", "localhost", "SMTP server domain")
smtpCmd.Flags().IntVar(&smtpWriteTimeout, "write-timeout", 10, "write timeout in seconds")
smtpCmd.Flags().IntVar(&smtpReadTimeout, "read-timeout", 10, "read timeout in seconds")
smtpCmd.Flags().IntVar(&smtpMaxMessageBytes, "max-message-bytes", 1024*1024, "max message size in bytes")
smtpCmd.Flags().IntVar(&smtpMaxRecipients, "max-recipients", 50, "maximum number of recipients per message")
smtpCmd.Flags().BoolVar(&smtpAllowInsecureAuth, "allow-insecure-auth", true, "allow authentication without TLS")
}
76 changes: 76 additions & 0 deletions cmd/smtps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"context"
"log/slog"
"time"

appconfig "github.com/lachlanharrisdev/gonetsim/internal/config"
"github.com/lachlanharrisdev/gonetsim/internal/observability"
"github.com/lachlanharrisdev/gonetsim/internal/service"
"github.com/lachlanharrisdev/gonetsim/internal/smtpserver"
"github.com/lachlanharrisdev/gonetsim/internal/utils"
"github.com/spf13/cobra"
)

var (
smtpsAddr string
smtpsDomain string
smtpsWriteTimeout int
smtpsReadTimeout int
smtpsMaxMessageBytes int
smtpsMaxRecipients int
smtpsAllowInsecureAuth bool
smtpsCert string
smtpsKey string
)

var smtpsCmd = &cobra.Command{
Use: "smtps",
Short: "Run an SMTPS server (secure SMTP with TLS)",
RunE: func(cmd *cobra.Command, args []string) error {
listen, err := parseAddrPort(smtpsAddr)
if err != nil {
return err
}

ctx, stop := utils.SignalContext(context.Background())
defer stop()

logger, err := observability.NewLogger(appconfig.Default().Logging)
if err != nil {
return err
}
slog.SetDefault(logger)
manager := service.NewManager(5*time.Second, logger)

return manager.RunSingleService(ctx,
smtpserver.NewSMTPSService(
smtpserver.Config{
Addr: listen,
Domain: smtpsDomain,
WriteTimeout: smtpsWriteTimeout,
ReadTimeout: smtpsReadTimeout,
MaxMessageBytes: smtpsMaxMessageBytes,
MaxRecipients: smtpsMaxRecipients,
AllowInsecureAuth: smtpsAllowInsecureAuth,
},
smtpserver.TLSOptions{CertFile: smtpsCert, KeyFile: smtpsKey},
),
)
},
}

func init() {
rootCmd.AddCommand(smtpsCmd)

smtpsCmd.Flags().StringVar(&smtpsAddr, "listen", ":1465", "listen address")
smtpsCmd.Flags().StringVar(&smtpsDomain, "domain", "localhost", "SMTP server domain")
smtpsCmd.Flags().IntVar(&smtpsWriteTimeout, "write-timeout", 10, "write timeout in seconds")
smtpsCmd.Flags().IntVar(&smtpsReadTimeout, "read-timeout", 10, "read timeout in seconds")
smtpsCmd.Flags().IntVar(&smtpsMaxMessageBytes, "max-message-bytes", 1024*1024, "max message size in bytes")
smtpsCmd.Flags().IntVar(&smtpsMaxRecipients, "max-recipients", 50, "maximum number of recipients per message")
smtpsCmd.Flags().BoolVar(&smtpsAllowInsecureAuth, "allow-insecure-auth", false, "allow authentication without TLS")
smtpsCmd.Flags().StringVar(&smtpsCert, "cert", "", "path to TLS cert PEM (optional; defaults to ephemeral self-signed)")
smtpsCmd.Flags().StringVar(&smtpsKey, "key", "", "path to TLS key PEM (optional; defaults to ephemeral self-signed)")
}
2 changes: 2 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ services:
- "5353:5353/tcp"
- "8080:8080"
- "8443:8443"
- "1025:1025"
- "1465:1465"

# by default the image includes a config at /etc/gonetsim/gonetsim.toml.
# to use a custom config, mount a read-only file over it:
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module github.com/lachlanharrisdev/gonetsim
go 1.26.1

require (
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.24.0
github.com/fatih/color v1.19.0
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/providers/structs v1.0.0
Expand All @@ -15,7 +18,6 @@ require (
)

require (
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
Expand Down Expand Up @@ -54,8 +58,6 @@ golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
Expand Down
63 changes: 62 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type Config struct {
DNS DNSConfig `koanf:"dns"`
HTTP HTTPConfig `koanf:"http"`
HTTPS HTTPSConfig `koanf:"https"`
SMTP SMTPConfig `koanf:"smtp"`
SMTPS SMTPSConfig `koanf:"smtps"`
Logging LoggingConfig `koanf:"logging"`
}

Expand Down Expand Up @@ -62,6 +64,30 @@ type HTTPSConfig struct {
Key string `koanf:"key"`
}

type SMTPConfig struct {
Enabled bool `koanf:"enabled"`
Addr string `koanf:"addr"` // ":1025"
Domain string `koanf:"domain"` // "localhost"
WriteTimeout int `koanf:"write_timeout"` // 10 seconds
ReadTimeout int `koanf:"read_timeout"` // 10 seconds
MaxMessageBytes int `koanf:"max_message_bytes"` // 1024 * 1024
MaxRecipients int `koanf:"max_recipients"` // 50
AllowInsecureAuth bool `koanf:"allow_insecure_auth"` // true
}

type SMTPSConfig struct {
Enabled bool `koanf:"enabled"`
Addr string `koanf:"addr"` // ":1465"
Domain string `koanf:"domain"` // "localhost"
WriteTimeout int `koanf:"write_timeout"` // 10 seconds
ReadTimeout int `koanf:"read_timeout"` // 10 seconds
MaxMessageBytes int `koanf:"max_message_bytes"` // 1024 * 1024
MaxRecipients int `koanf:"max_recipients"` // 50
AllowInsecureAuth bool `koanf:"allow_insecure_auth"` // false (secure)
Cert string `koanf:"cert"` // Optional TLS cert
Key string `koanf:"key"` // Optional TLS key
}

type LoggingConfig struct {
LogFormat string `koanf:"format"`
Level string `koanf:"level"`
Expand Down Expand Up @@ -91,6 +117,26 @@ func Default() Config {
Listen: ":8443",
Status: 200,
},
SMTP: SMTPConfig{
Enabled: true,
Addr: ":1025",
Domain: "localhost",
WriteTimeout: 10,
ReadTimeout: 10,
MaxMessageBytes: 1024 * 1024,
MaxRecipients: 50,
AllowInsecureAuth: true,
},
SMTPS: SMTPSConfig{
Enabled: false,
Addr: ":1465",
Domain: "localhost",
WriteTimeout: 10,
ReadTimeout: 10,
MaxMessageBytes: 1024 * 1024,
MaxRecipients: 50,
AllowInsecureAuth: false,
},
Logging: LoggingConfig{
LogFormat: "text",
Level: "info",
Expand Down Expand Up @@ -130,7 +176,22 @@ func (c Config) Validate() error {
}
}

if !c.DNS.Enabled && !c.HTTP.Enabled && !c.HTTPS.Enabled {
if c.SMTP.Enabled {
if c.SMTP.Addr == "" {
return errors.New("smtp.addr is required when smtp.enabled=true")
}
}

if c.SMTPS.Enabled {
if c.SMTPS.Addr == "" {
return errors.New("smtps.addr is required when smtps.enabled=true")
}
if (c.SMTPS.Cert == "") != (c.SMTPS.Key == "") {
return errors.New("smtps.cert and smtps.key must be set together")
}
}

if !c.DNS.Enabled && !c.HTTP.Enabled && !c.HTTPS.Enabled && !c.SMTP.Enabled && !c.SMTPS.Enabled {
return errors.New("at least one service must be enabled")
}

Expand Down
21 changes: 19 additions & 2 deletions internal/config/default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,25 @@ listen = ":8443"
# Status code to return for all requests. 0 means default (200).
status = 200

# Optional TLS cert/key paths (PEM). If both are empty, gonetsim generates an
# ephemeral self-signed certificate at startup.
[smtp]
enabled = true
addr = ":1025"
domain = "localhost"
write_timeout = 10
read_timeout = 10
max_message_bytes = 1048576
max_recipients = 50
allow_insecure_auth = true

[smtps]
enabled = false
addr = ":1465"
domain = "localhost"
write_timeout = 10
read_timeout = 10
max_message_bytes = 1048576
max_recipients = 50
allow_insecure_auth = false
cert = ""
key = ""

Expand Down
Loading