3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-11 06:29:29 +01:00

Merge pull request #694 from slingamn/issue688_forward_confirmed_dns.4

#688: forward-confirmed reverse dns
This commit is contained in:
Shivaram Lingamneni 2019-12-18 10:22:50 -05:00 committed by GitHub
commit 9d56677691
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 71 deletions

View File

@ -259,11 +259,10 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
// cover up details of the tor proxying infrastructure (not a user privacy concern, // cover up details of the tor proxying infrastructure (not a user privacy concern,
// but a hardening measure): // but a hardening measure):
session.proxiedIP = utils.IPv4LoopbackAddress session.proxiedIP = utils.IPv4LoopbackAddress
client.proxiedIP = session.proxiedIP
session.rawHostname = config.Server.TorListeners.Vhost session.rawHostname = config.Server.TorListeners.Vhost
client.rawHostname = session.rawHostname
} else { } else {
// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
session.rawHostname = utils.LookupHostname(session.realIP.String())
client.cloakedHostname = config.Server.Cloaks.ComputeCloak(session.realIP)
remoteAddr := conn.Conn.RemoteAddr() remoteAddr := conn.Conn.RemoteAddr()
if utils.AddrIsLocal(remoteAddr) { if utils.AddrIsLocal(remoteAddr) {
// treat local connections as secure (may be overridden later by WEBIRC) // treat local connections as secure (may be overridden later by WEBIRC)
@ -274,13 +273,71 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
} }
} }
client.realIP = session.realIP client.realIP = session.realIP
client.rawHostname = session.rawHostname
client.proxiedIP = session.proxiedIP
server.stats.Add() server.stats.Add()
client.run(session, proxyLine) client.run(session, proxyLine)
} }
// resolve an IP to an IRC-ready hostname, using reverse DNS, forward-confirming if necessary,
// and sending appropriate notices to the client
func (client *Client) lookupHostname(session *Session, overwrite bool) {
if client.isTor {
return
} // else: even if cloaking is enabled, look up the real hostname to show to operators
config := client.server.Config()
ip := session.realIP
if session.proxiedIP != nil {
ip = session.proxiedIP
}
ipString := ip.String()
var hostname, candidate string
if config.Server.lookupHostnames {
session.Notice("*** Looking up your hostname...")
names, err := net.LookupAddr(ipString)
if err == nil && 0 < len(names) {
candidate = strings.TrimSuffix(names[0], ".")
}
if utils.IsHostname(candidate) {
if config.Server.ForwardConfirmHostnames {
addrs, err := net.LookupHost(candidate)
if err == nil {
for _, addr := range addrs {
if addr == ipString {
hostname = candidate // successful forward confirmation
break
}
}
}
} else {
hostname = candidate
}
}
}
if hostname != "" {
session.Notice("*** Found your hostname")
} else {
if config.Server.lookupHostnames {
session.Notice("*** Couldn't look up your hostname")
}
hostname = utils.IPStringToHostname(ipString)
}
session.rawHostname = hostname
cloakedHostname := config.Server.Cloaks.ComputeCloak(ip)
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
// update the hostname if this is a new connection or a resume, but not if it's a reattach
if overwrite || client.rawHostname == "" {
client.rawHostname = hostname
client.cloakedHostname = cloakedHostname
client.updateNickMaskNoMutex()
}
}
func (client *Client) doIdentLookup(conn net.Conn) { func (client *Client) doIdentLookup(conn net.Conn) {
_, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String()) _, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil { if err != nil {
@ -617,7 +674,7 @@ func (session *Session) playResume() {
details := client.Details() details := client.Details()
oldNickmask := details.nickMask oldNickmask := details.nickMask
client.SetRawHostname(session.rawHostname) client.lookupHostname(session, true)
hostname := client.Hostname() // may be a vhost hostname := client.Hostname() // may be a vhost
timestampString := timestamp.Format(IRCv3TimestampFormat) timestampString := timestamp.Format(IRCv3TimestampFormat)
@ -887,6 +944,10 @@ func (client *Client) updateNickMaskNoMutex() {
} }
} }
if client.hostname == "" {
return // pre-registration, don't bother generating the hostname
}
cfhostname, err := Casefold(client.hostname) cfhostname, err := Casefold(client.hostname)
if err != nil { if err != nil {
client.server.logger.Error("internal", "hostname couldn't be casefolded", client.hostname, err.Error()) client.server.logger.Error("internal", "hostname couldn't be casefolded", client.hostname, err.Error())
@ -1255,6 +1316,10 @@ func (client *Client) Notice(text string) {
client.Send(nil, client.server.name, "NOTICE", client.Nick(), text) client.Send(nil, client.server.name, "NOTICE", client.Nick(), text)
} }
func (session *Session) Notice(text string) {
session.Send(nil, session.client.server.name, "NOTICE", session.client.Nick(), text)
}
func (client *Client) addChannel(channel *Channel) { func (client *Client) addChannel(channel *Channel) {
client.stateMutex.Lock() client.stateMutex.Lock()
client.channels[channel] = true client.channels[channel] = true

View File

@ -293,19 +293,22 @@ type Config struct {
Listen []string Listen []string
TLSListeners map[string]TLSListenConfig `yaml:"tls-listeners"` TLSListeners map[string]TLSListenConfig `yaml:"tls-listeners"`
// either way, the result is this: // either way, the result is this:
trueListeners map[string]listenerConfig trueListeners map[string]listenerConfig
STS STSConfig STS STSConfig
CheckIdent bool `yaml:"check-ident"` LookupHostnames *bool `yaml:"lookup-hostnames"`
MOTD string lookupHostnames bool
motdLines []string ForwardConfirmHostnames bool `yaml:"forward-confirm-hostnames"`
MOTDFormatting bool `yaml:"motd-formatting"` CheckIdent bool `yaml:"check-ident"`
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"` MOTD string
proxyAllowedFromNets []net.IPNet motdLines []string
WebIRC []webircConfig `yaml:"webirc"` MOTDFormatting bool `yaml:"motd-formatting"`
MaxSendQString string `yaml:"max-sendq"` ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
MaxSendQBytes int proxyAllowedFromNets []net.IPNet
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"` WebIRC []webircConfig `yaml:"webirc"`
Compatibility struct { MaxSendQString string `yaml:"max-sendq"`
MaxSendQBytes int
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"`
Compatibility struct {
ForceTrailing *bool `yaml:"force-trailing"` ForceTrailing *bool `yaml:"force-trailing"`
forceTrailing bool forceTrailing bool
SendUnprefixedSasl bool `yaml:"send-unprefixed-sasl"` SendUnprefixedSasl bool `yaml:"send-unprefixed-sasl"`
@ -590,7 +593,7 @@ func LoadConfig(filename string) (config *Config, err error) {
if config.Server.Name == "" { if config.Server.Name == "" {
return nil, ErrServerNameMissing return nil, ErrServerNameMissing
} }
if !utils.IsHostname(config.Server.Name) { if !utils.IsServerName(config.Server.Name) {
return nil, ErrServerNameNotHostname return nil, ErrServerNameNotHostname
} }
if config.Datastore.Path == "" { if config.Datastore.Path == "" {
@ -635,6 +638,13 @@ func LoadConfig(filename string) (config *Config, err error) {
// set this even if STS is disabled // set this even if STS is disabled
config.Server.capValues[caps.STS] = config.Server.STS.Value() config.Server.capValues[caps.STS] = config.Server.STS.Value()
// lookup-hostnames defaults to true if unset
if config.Server.LookupHostnames != nil {
config.Server.lookupHostnames = *config.Server.LookupHostnames
} else {
config.Server.lookupHostnames = true
}
// process webirc blocks // process webirc blocks
var newWebIRC []webircConfig var newWebIRC []webircConfig
for _, webirc := range config.Server.WebIRC { for _, webirc := range config.Server.WebIRC {

View File

@ -76,18 +76,12 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
client.server.connectionLimiter.RemoveClient(session.realIP) client.server.connectionLimiter.RemoveClient(session.realIP)
// given IP is sane! override the client's current IP // given IP is sane! override the client's current IP
ipstring := parsedProxiedIP.String() client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", parsedProxiedIP.String())
client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring)
rawHostname := utils.LookupHostname(ipstring)
cloakedHostname := client.server.Config().Server.Cloaks.ComputeCloak(parsedProxiedIP)
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
client.proxiedIP = parsedProxiedIP client.proxiedIP = parsedProxiedIP
client.rawHostname = rawHostname
session.proxiedIP = parsedProxiedIP session.proxiedIP = parsedProxiedIP
session.rawHostname = rawHostname
client.cloakedHostname = cloakedHostname
// nickmask will be updated when the client completes registration // nickmask will be updated when the client completes registration
// set tls info // set tls info
client.certfp = "" client.certfp = ""

View File

@ -239,14 +239,6 @@ func (client *Client) RawHostname() (result string) {
return return
} }
func (client *Client) SetRawHostname(rawHostname string) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.rawHostname = rawHostname
client.updateNickMaskNoMutex()
}
func (client *Client) AwayMessage() (result string) { func (client *Client) AwayMessage() (result string) {
client.stateMutex.RLock() client.stateMutex.RLock()
result = client.awayMessage result = client.awayMessage

View File

@ -802,16 +802,16 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
target := client target := client
if 0 < len(params) { if 0 < len(params) {
// same permissions check as RPL_WHOISACTUALLY for now:
if !client.HasMode(modes.Operator) {
nsNotice(rb, client.t("Command restricted"))
return
}
target = server.clients.Get(params[0]) target = server.clients.Get(params[0])
if target == nil { if target == nil {
nsNotice(rb, client.t("No such nick")) nsNotice(rb, client.t("No such nick"))
return return
} }
// same permissions check as RPL_WHOISACTUALLY for now:
if target != client && !client.HasMode(modes.Operator) {
nsNotice(rb, client.t("Command restricted"))
return
}
} }
sessionData, currentIndex := target.AllSessionData(rb.session) sessionData, currentIndex := target.AllSessionData(rb.session)

View File

@ -377,12 +377,9 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
return return
} }
// check KLINEs // we have nickname, username, and the final value of the IP address:
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) // do the hostname lookup and set the nickmask
if isBanned { session.client.lookupHostname(session, false)
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
return true
}
if session.client != c { if session.client != c {
// reattached, bail out. // reattached, bail out.
@ -392,6 +389,13 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
return return
} }
// check KLINEs
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
if isBanned {
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
return true
}
// registration has succeeded: // registration has succeeded:
c.SetRegistered() c.SetRegistered()

View File

@ -6,6 +6,7 @@ package utils
import ( import (
"net" "net"
"regexp"
"strings" "strings"
) )
@ -13,6 +14,8 @@ var (
// subnet mask for an ipv6 /128: // subnet mask for an ipv6 /128:
mask128 = net.CIDRMask(128, 128) mask128 = net.CIDRMask(128, 128)
IPv4LoopbackAddress = net.ParseIP("127.0.0.1").To16() IPv4LoopbackAddress = net.ParseIP("127.0.0.1").To16()
validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`)
) )
// AddrIsLocal returns whether the address is from a trusted local connection (loopback or unix). // AddrIsLocal returns whether the address is from a trusted local connection (loopback or unix).
@ -40,30 +43,19 @@ func AddrIsUnix(addr net.Addr) bool {
return ok return ok
} }
// LookupHostname returns the hostname for `addr` if it has one. Otherwise, just returns `addr`. // IPStringToHostname converts a string representation of an IP address to an IRC-ready hostname
func LookupHostname(addr string) string { func IPStringToHostname(ipStr string) string {
names, err := net.LookupAddr(addr) if 0 < len(ipStr) && ipStr[0] == ':' {
if err == nil && len(names) > 0 {
candidate := strings.TrimSuffix(names[0], ".")
if IsHostname(candidate) {
return candidate
}
}
// return original address if no hostname found
if len(addr) > 0 && addr[0] == ':' {
// fix for IPv6 hostnames (so they don't start with a colon), same as all other IRCds // fix for IPv6 hostnames (so they don't start with a colon), same as all other IRCds
addr = "0" + addr ipStr = "0" + ipStr
} }
return addr return ipStr
} }
var allowedHostnameChars = "abcdefghijklmnopqrstuvwxyz1234567890-."
// IsHostname returns whether we consider `name` a valid hostname. // IsHostname returns whether we consider `name` a valid hostname.
func IsHostname(name string) bool { func IsHostname(name string) bool {
// IRC hostnames specifically require a period name = strings.TrimSuffix(name, ".")
if !strings.Contains(name, ".") || len(name) < 1 || len(name) > 253 { if len(name) < 1 || len(name) > 253 {
return false return false
} }
@ -72,11 +64,7 @@ func IsHostname(name string) bool {
if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") { if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
return false return false
} }
} if !validHostnameLabelRegexp.MatchString(part) {
// ensure all chars of hostname are valid
for _, char := range strings.Split(strings.ToLower(name), "") {
if !strings.Contains(allowedHostnameChars, char) {
return false return false
} }
} }
@ -84,6 +72,12 @@ func IsHostname(name string) bool {
return true return true
} }
// IsServerName returns whether we consider `name` a valid IRC server name.
func IsServerName(name string) bool {
// IRC server names specifically require a period
return IsHostname(name) && strings.IndexByte(name, '.') != -1
}
// Convenience to test whether `ip` is contained in any of `nets`. // Convenience to test whether `ip` is contained in any of `nets`.
func IPInNets(ip net.IP, nets []net.IPNet) bool { func IPInNets(ip net.IP, nets []net.IPNet) bool {
for _, network := range nets { for _, network := range nets {

View File

@ -24,14 +24,18 @@ var (
"gsf.ds342.co.uk", "gsf.ds342.co.uk",
"324.net.uk", "324.net.uk",
"xn--bcher-kva.ch", "xn--bcher-kva.ch",
"pentos",
"pentos.",
"www.google.com.",
} }
badHostnames = []string{ badHostnames = []string{
"-lol-.net.uk", "-lol-.net.uk",
"-lol.net.uk", "-lol.net.uk",
"_irc._sctp.lol.net.uk", "_irc._sctp.lol.net.uk",
"irc", "irc.l%l.net.uk",
"com", "irc..net.uk",
".",
"", "",
} }
) )
@ -56,6 +60,15 @@ func TestIsHostname(t *testing.T) {
} }
} }
func TestIsServerName(t *testing.T) {
if IsServerName("pentos") {
t.Error("irc server names must contain a period")
}
if !IsServerName("darwin.network") {
t.Error("failed to validate a perfectly good server name")
}
}
func TestNormalizeToNet(t *testing.T) { func TestNormalizeToNet(t *testing.T) {
a := net.ParseIP("8.8.8.8") a := net.ParseIP("8.8.8.8")
b := net.ParseIP("8.8.4.4") b := net.ParseIP("8.8.4.4")

View File

@ -85,6 +85,14 @@ server:
# should clients include this STS policy when they ship their inbuilt preload lists? # should clients include this STS policy when they ship their inbuilt preload lists?
preload: false preload: false
# whether to look up user hostnames with reverse DNS
# (to suppress this for privacy purposes, use the ip-cloaking options below)
lookup-hostnames: true
# whether to confirm hostname lookups using "forward-confirmed reverse DNS", i.e., for
# any hostname returned from reverse DNS, resolve it back to an IP address and reject it
# unless it matches the connecting IP
forward-confirm-hostnames: true
# use ident protocol to get usernames # use ident protocol to get usernames
check-ident: false check-ident: false