Skip to content
Draft
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
18 changes: 15 additions & 3 deletions example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ concurrency = 8192
prefer-ip = "prefer-ipv6"

# Public IP addresses of this server. Used by 'mtg access' to generate
# proxy links and by 'mtg doctor' to validate SNI-DNS match.
# If not set, mtg tries to detect them automatically via ifconfig.co.
# Set these if ifconfig.co is unreachable from your server.
# proxy links and by 'mtg doctor' / proxy startup to validate SNI-DNS match.
# If not set, mtg tries to detect them automatically by querying the public
# HTTPS endpoints listed in network.public-ip-endpoints (see below).
# Set these explicitly if those endpoints are unreachable from your server.
# public-ipv4 = "1.2.3.4"
# public-ipv6 = "2001:db8::1"

Expand Down Expand Up @@ -200,6 +201,17 @@ proxies = [
# "socks5://user:password@host:port"
]

# HTTPS endpoints used to discover this server's public IPv4/IPv6 when
# public-ipv4 / public-ipv6 are not set. Each must return the client's public
# IP as a single address in the plain-text response body. mtg tries them in
# order and uses the first that succeeds. The default is shown below; setting
# this option overrides the default entirely.
# public-ip-endpoints = [
# "https://ifconfig.co",
# "https://icanhazip.com",
# "https://ifconfig.me",
# ]

# network timeouts define different settings for timeouts. tcp timeout
# define a global timeout on establishing of network connections. idle
# means a timeout on pumping data between sockset when nothing is
Expand Down
10 changes: 8 additions & 2 deletions internal/cli/access.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"strconv"
"sync"
"time"

"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/utils"
Expand Down Expand Up @@ -54,6 +56,10 @@ func (a *Access) Run(cli *CLI, version string) error {
return fmt.Errorf("cannot init network: %w", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

endpoints := resolvePublicIPEndpoints(conf.Network.PublicIPEndpoints)
wg := &sync.WaitGroup{}

wg.Go(func() {
Expand All @@ -62,7 +68,7 @@ func (a *Access) Run(cli *CLI, version string) error {
ip = conf.PublicIPv4.Get(nil)
}
if ip == nil {
ip = getIP(ntw, "tcp4")
ip = getIP(ctx, ntw, "tcp4", endpoints)
}

if ip != nil {
Expand All @@ -77,7 +83,7 @@ func (a *Access) Run(cli *CLI, version string) error {
ip = conf.PublicIPv6.Get(nil)
}
if ip == nil {
ip = getIP(ntw, "tcp6")
ip = getIP(ctx, ntw, "tcp6", endpoints)
}

if ip != nil {
Expand Down
89 changes: 58 additions & 31 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ var (
)

tplODNSSNIMatch = template.Must(
template.New("").Parse(" ✅ IP address {{ .ip }} matches secret hostname {{ .hostname }}\n"),
template.New("").Parse(" ✅ Secret hostname {{ .hostname }} matches our public IP ({{ .our }}); resolved: {{ .resolved }}\n"),
)
tplEDNSSNIMatch = template.Must(
template.New("").Parse(" ❌ Hostname {{ .hostname }} {{ if .resolved }}is resolved to {{ .resolved }} addresses, not {{ if .ip4 }}{{ .ip4 }}{{ else }}{{ .ip6 }}{{ end }}{{ else }}cannot be resolved to any host{{ end }}\n"),
template.New("").Parse(" ❌ Secret hostname {{ .hostname }} resolves to {{ .resolved }} but our public IP is {{ .our }}{{ if .families }} (mismatched families: {{ .families }}){{ end }}\n"),
)
tplEDNSSNINoResolve = template.Must(
template.New("").Parse(" ❌ Secret hostname {{ .hostname }} cannot be resolved to any address\n"),
)

tplOFrontingDomain = template.Must(
Expand Down Expand Up @@ -329,52 +332,76 @@ func (d *Doctor) checkFrontingDomain(ntw mtglib.Network) bool {
}

func (d *Doctor) checkSecretHost(resolver *net.Resolver, ntw mtglib.Network) bool {
addresses, err := resolver.LookupIPAddr(context.Background(), d.conf.Secret.Host)
if err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

res := runSNICheck(ctx, resolver, d.conf, ntw)

if res.ResolveErr != nil {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": fmt.Sprintf("cannot resolve DNS name of %s", d.conf.Secret.Host),
"error": err,
"description": fmt.Sprintf("cannot resolve DNS name of %s", res.Host),
"error": res.ResolveErr,
})
return false
}

ourIP4 := d.conf.PublicIPv4.Get(nil)
if ourIP4 == nil {
ourIP4 = getIP(ntw, "tcp4")
}

ourIP6 := d.conf.PublicIPv6.Get(nil)
if ourIP6 == nil {
ourIP6 = getIP(ntw, "tcp6")
}

if ourIP4 == nil && ourIP6 == nil {
if !res.Known() {
tplError.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"description": "cannot detect public IP address",
"error": errors.New("cannot detect automatically and public-ipv4/public-ipv6 are not set in config"),
})
return false
}

strAddresses := []string{}
for _, value := range addresses {
if (ourIP4 != nil && value.IP.String() == ourIP4.String()) ||
(ourIP6 != nil && value.IP.String() == ourIP6.String()) {
tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"ip": value.IP,
"hostname": d.conf.Secret.Host,
})
return true
if len(res.Resolved) == 0 {
tplEDNSSNINoResolve.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"hostname": res.Host,
})
return false
}

resolved := make([]string, 0, len(res.Resolved))
for _, ip := range res.Resolved {
resolved = append(resolved, `"`+ip.String()+`"`)
}

our := ""
if res.OurIPv4 != nil {
our = res.OurIPv4.String()
}

if res.OurIPv6 != nil {
if our != "" {
our += "/"
}

strAddresses = append(strAddresses, `"`+value.IP.String()+`"`)
our += res.OurIPv6.String()
}

if res.OK() {
tplODNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"hostname": res.Host,
"resolved": strings.Join(resolved, ", "),
"our": our,
})
return true
}

mismatched := []string{}

if res.OurIPv4 != nil && !res.IPv4Match {
mismatched = append(mismatched, "IPv4")
}

if res.OurIPv6 != nil && !res.IPv6Match {
mismatched = append(mismatched, "IPv6")
}

tplEDNSSNIMatch.Execute(os.Stdout, map[string]any{ //nolint: errcheck
"hostname": d.conf.Secret.Host,
"resolved": strings.Join(strAddresses, ", "),
"ip4": ourIP4,
"ip6": ourIP6,
"hostname": res.Host,
"resolved": strings.Join(resolved, ", "),
"our": our,
"families": strings.Join(mismatched, ", "),
})

return false
Expand Down
67 changes: 27 additions & 40 deletions internal/cli/run_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
"os"
"strings"
"time"

"github.com/9seconds/mtg/v2/antireplay"
"github.com/9seconds/mtg/v2/events"
Expand Down Expand Up @@ -209,78 +210,64 @@ func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStr
}

func warnSNIMismatch(conf *config.Config, ntw mtglib.Network, log mtglib.Logger) {
host := conf.Secret.Host
if host == "" {
if conf.Secret.Host == "" {
return
}

addresses, err := net.DefaultResolver.LookupIPAddr(context.Background(), host)
if err != nil {
log.BindStr("hostname", host).
WarningError("SNI-DNS check: cannot resolve secret hostname", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

ourIP4 := conf.PublicIPv4.Get(nil)
if ourIP4 == nil {
ourIP4 = getIP(ntw, "tcp4")
}
res := runSNICheck(ctx, net.DefaultResolver, conf, ntw)

ourIP6 := conf.PublicIPv6.Get(nil)
if ourIP6 == nil {
ourIP6 = getIP(ntw, "tcp6")
if res.ResolveErr != nil {
log.BindStr("hostname", res.Host).
WarningError("SNI-DNS check: cannot resolve secret hostname", res.ResolveErr)
return
}

if ourIP4 == nil && ourIP6 == nil {
if !res.Known() {
log.Warning("SNI-DNS check: cannot detect public IP address; set public-ipv4/public-ipv6 in config or run 'mtg doctor'")
return
}

v4Match := ourIP4 == nil
v6Match := ourIP6 == nil

for _, addr := range addresses {
if ourIP4 != nil && addr.IP.String() == ourIP4.String() {
v4Match = true
}

if ourIP6 != nil && addr.IP.String() == ourIP6.String() {
v6Match = true
}
if len(res.Resolved) == 0 {
log.BindStr("hostname", res.Host).
Warning("SNI-DNS check: secret hostname does not resolve to any address")
return
}

if v4Match && v6Match {
if res.OK() {
return
}

resolved := make([]string, 0, len(addresses))
for _, addr := range addresses {
resolved = append(resolved, addr.IP.String())
resolved := make([]string, 0, len(res.Resolved))
for _, ip := range res.Resolved {
resolved = append(resolved, ip.String())
}

our := ""
if ourIP4 != nil {
our = ourIP4.String()
if res.OurIPv4 != nil {
our = res.OurIPv4.String()
}

if ourIP6 != nil {
if res.OurIPv6 != nil {
if our != "" {
our += "/"
}

our += ourIP6.String()
our += res.OurIPv6.String()
}

entry := log.BindStr("hostname", host).
entry := log.BindStr("hostname", res.Host).
BindStr("resolved", strings.Join(resolved, ", ")).
BindStr("public_ip", our)

if ourIP4 != nil {
entry = entry.BindStr("ipv4_match", fmt.Sprintf("%t", v4Match))
if res.OurIPv4 != nil {
entry = entry.BindStr("ipv4_match", fmt.Sprintf("%t", res.IPv4Match))
}

if ourIP6 != nil {
entry = entry.BindStr("ipv6_match", fmt.Sprintf("%t", v6Match))
if res.OurIPv6 != nil {
entry = entry.BindStr("ipv6_match", fmt.Sprintf("%t", res.IPv6Match))
}

entry.Warning("SNI-DNS mismatch: secret hostname does not resolve to this server's public IP. " +
Expand Down
Loading
Loading