diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 5d9c957e..1422dd1e 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -32,8 +32,10 @@ _Copyright © 2018 Daniel Oaks _ - Channel Modes - Channel Prefixes - Commands -- Integrating with other software +- Working with other software - HOPM + - ZNC + - Tor - Acknowledgements @@ -541,7 +543,7 @@ We may add some additional notes here for specific commands down the line, but r -------------------------------------------------------------------------------------------- -# Integrating with other software +# Working with other software Oragono should interoperate with most IRC-based software, including bots. If you have problems getting your preferred software to work with Oragono, feel free to report it to us. If the root cause is a bug in Oragono, we'll fix it. @@ -601,6 +603,50 @@ kline = "DLINE ANDKILL 2h %i :Open proxy found on your host."; Versions of ZNC prior to 1.7 have a [bug](https://github.com/znc/znc/issues/1212) in their SASL implementation that renders them incompatible with Oragono. However, you should be able to authenticate from ZNC using its [NickServ](https://wiki.znc.in/Nickserv) module. +## Tor + +Oragono has code support for adding an .onion address to an IRC server, or operating an IRC server as a Tor hidden service. This is subtle, so you should be familiar with the [Tor Project](https://www.torproject.org/) and the concept of a [hidden service](https://www.torproject.org/docs/tor-onion-service.html.en). + +There are two possible ways to serve Oragono over Tor. One is to add a .onion address to a server that also serves non-Tor clients, and whose IP address is public information. This is relatively straightforward. Add a separate listener, for example `127.0.0.2:6668`, to Oragono's `server.listen`, then add it to `server.tor-listeners.listeners`. Then configure Tor like this: + +```` +HiddenServiceDir /var/lib/tor/oragono_hidden_service +HiddenServicePort 6667 127.0.0.2:6668 + +# these are optional, but can be used to speed up the circuits in the case +# where the server's own IP is public information (clients will remain anonymous): +HiddenServiceNonAnonymousMode 1 +HiddenServiceSingleHopMode 1 +```` + +Tor provides end-to-end encryption for hidden services, so there's no need to enable TLS in Oragono for the listener (`127.0.0.2:6668` in this example). Doing so is not recommended, given the difficulty in obtaining a TLS certificate valid for an .onion address. + +The second way is to run Oragono as a true hidden service, where the server's actual IP address is a secret. This requires hardening measures on the Oragono side: + +* Oragono should not accept any connections on its public interfaces. You should remove any listener that starts with the address of a public interface, or with `:`, which means "listen on all available interfaces". You should listen only on `127.0.0.1:6667` and a Unix domain socket such as `/hidden_service_sockets/oragono.sock`. +* In this mode, it is especially important that all operator passwords are strong and all operators are trusted (operators have a larger attack surface to deanonymize the server). +* Tor hidden services are at risk of being deanonymized if a client can trick the server into performing a non-Tor network request. Oragono should not perform any such requests (such as hostname resolution or ident lookups) in response to input received over a correctly configured Tor listener. However, Oragono has not been thoroughly audited against such deanonymization attacks --- therefore, Oragono should be deployed with additional sandboxing to protect against this: + * Oragono should run with no direct network connectivity, e.g., by running in its own Linux network namespace. systemd implements this with the [PrivateNetwork](https://www.freedesktop.org/software/systemd/man/systemd.exec.html) configuration option: add `PrivateNetwork=true` to Oragono's systemd unit file. + * Since the loopback adapters are local to a specific network namespace, and the Tor daemon will run in the root namespace, Tor will be unable to connect to Oragono over loopback TCP. Instead, Oragono must listen on a named Unix domain socket that the Tor daemon can connect to. However, distributions typically package Tor with its own hardening profiles, which restrict which sockets it can access. Below is a recipe for configuring this with the official Tor packages for Debian: + +1. Create a directory with `0777` permissions such as `/hidden_service_sockets`. +1. Configure Oragono to listen on `/hidden_service_sockets/oragono.sock`, and add this socket to `server.tor-listeners.listeners`. +1. Ensure that Oragono has no direct network access as described above, e.g., with `PrivateNetwork=true`. +1. Next, modify Tor's apparmor profile so that it can connect to this socket, by adding the line ` /hidden_service_sockets/** rw,` to `/etc/apparmor.d/local/system_tor`. +1. Finally, configure Tor with: + +```` +HiddenServiceDir /var/lib/tor/oragono_hidden_service +HiddenServicePort 6667 unix:/hidden_service_sockets/oragono.sock +# DO NOT enable HiddenServiceNonAnonymousMode +```` + +Instructions on how client software should connect to an .onion address are outside the scope of this manual. However: + +1. [Hexchat](https://hexchat.github.io/) is known to support .onion addresses, once it has been configured to use a local Tor daemon as a SOCKS proxy (Settings -> Preferences -> Network Setup -> Proxy Server). +1. Pidgin should work with [torsocks](https://trac.torproject.org/projects/tor/wiki/doc/torsocks). + + -------------------------------------------------------------------------------------------- diff --git a/irc/client.go b/irc/client.go index 99d8df2e..3ae9ac22 100644 --- a/irc/client.go +++ b/irc/client.go @@ -65,6 +65,7 @@ type Client struct { idletimer *IdleTimer invitedTo map[string]bool isDestroyed bool + isTor bool isQuitting bool languages []string loginThrottle connection_limits.GenericThrottle @@ -117,12 +118,12 @@ type ClientDetails struct { accountName string } -// NewClient sets up a new client and starts its goroutine. -func NewClient(server *Server, conn net.Conn, isTLS bool) { +// NewClient sets up a new client and runs its goroutine. +func RunNewClient(server *Server, conn clientConn) { now := time.Now() config := server.Config() fullLineLenLimit := config.Limits.LineLen.Tags + config.Limits.LineLen.Rest - socket := NewSocket(conn, fullLineLenLimit*2, config.Server.MaxSendQBytes) + socket := NewSocket(conn.Conn, fullLineLenLimit*2, config.Server.MaxSendQBytes) client := &Client{ atime: now, capabilities: caps.NewSet(), @@ -131,6 +132,7 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) { channels: make(ChannelSet), ctime: now, flags: modes.NewModeSet(), + isTor: conn.IsTor, languages: server.Languages().Default(), loginThrottle: connection_limits.GenericThrottle{ Duration: config.Accounts.LoginThrottling.Duration, @@ -145,58 +147,73 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) { history: history.NewHistoryBuffer(config.History.ClientLength), } - remoteAddr := conn.RemoteAddr() - client.realIP = utils.AddrToIP(remoteAddr) - if client.realIP == nil { - server.logger.Error("internal", "bad remote address", remoteAddr.String()) - return - } client.recomputeMaxlens() - if isTLS { - client.SetMode(modes.TLS, true) + if conn.IsTLS { + client.SetMode(modes.TLS, true) // error is not useful to us here anyways so we can ignore it client.certfp, _ = client.socket.CertFP() } - if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) { - _, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String()) - if err != nil { - server.logger.Error("internal", "bad server address", err.Error()) - return - } - serverPort, _ := strconv.Atoi(serverPortString) - clientHost, clientPortString, err := net.SplitHostPort(conn.RemoteAddr().String()) - if err != nil { - server.logger.Error("internal", "bad client address", err.Error()) - return - } - clientPort, _ := strconv.Atoi(clientPortString) - client.Notice(client.t("*** Looking up your username")) - resp, err := ident.Query(clientHost, serverPort, clientPort, IdentTimeoutSeconds) - if err == nil { - err := client.SetNames(resp.Identifier, "", true) - if err == nil { - client.Notice(client.t("*** Found your username")) - // we don't need to updateNickMask here since nickMask is not used for anything yet - } else { - client.Notice(client.t("*** Got a malformed username, ignoring")) - } - } else { - client.Notice(client.t("*** Could not find your username")) + if conn.IsTor { + client.SetMode(modes.TLS, true) + client.realIP = utils.IPv4LoopbackAddress + client.rawHostname = config.Server.TorListeners.Vhost + } else { + remoteAddr := conn.Conn.RemoteAddr() + client.realIP = utils.AddrToIP(remoteAddr) + // Set the hostname for this client + // (may be overridden by a later PROXY command from stunnel) + client.rawHostname = utils.LookupHostname(client.realIP.String()) + if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) { + client.doIdentLookup(conn.Conn) } } - go client.run() + + client.run() +} + +func (client *Client) doIdentLookup(conn net.Conn) { + _, serverPortString, err := net.SplitHostPort(conn.LocalAddr().String()) + if err != nil { + client.server.logger.Error("internal", "bad server address", err.Error()) + return + } + serverPort, _ := strconv.Atoi(serverPortString) + clientHost, clientPortString, err := net.SplitHostPort(conn.RemoteAddr().String()) + if err != nil { + client.server.logger.Error("internal", "bad client address", err.Error()) + return + } + clientPort, _ := strconv.Atoi(clientPortString) + + client.Notice(client.t("*** Looking up your username")) + resp, err := ident.Query(clientHost, serverPort, clientPort, IdentTimeoutSeconds) + if err == nil { + err := client.SetNames(resp.Identifier, "", true) + if err == nil { + client.Notice(client.t("*** Found your username")) + // we don't need to updateNickMask here since nickMask is not used for anything yet + } else { + client.Notice(client.t("*** Got a malformed username, ignoring")) + } + } else { + client.Notice(client.t("*** Could not find your username")) + } } func (client *Client) isAuthorized(config *Config) bool { saslSent := client.account != "" - passRequirementMet := (config.Server.passwordBytes == nil) || client.sentPassCommand || (config.Accounts.SkipServerPassword && saslSent) - if !passRequirementMet { + // PASS requirement + if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) { return false } - saslRequirementMet := !config.Accounts.RequireSasl.Enabled || saslSent || utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets) - return saslRequirementMet + // Tor connections may be required to authenticate with SASL + if client.isTor && config.Server.TorListeners.RequireSasl && !saslSent { + return false + } + // finally, enforce require-sasl + return !config.Accounts.RequireSasl.Enabled || saslSent || utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets) } func (client *Client) resetFakelag() { @@ -296,10 +313,6 @@ func (client *Client) run() { client.resetFakelag() - // Set the hostname for this client - // (may be overridden by a later PROXY command from stunnel) - client.rawHostname = utils.LookupHostname(client.realIP.String()) - firstLine := true for { @@ -315,7 +328,9 @@ func (client *Client) run() { break } - client.server.logger.Debug("userinput", client.nick, "<- ", line) + if client.server.logger.IsLoggingRawIO() { + client.server.logger.Debug("userinput", client.nick, "<- ", line) + } // special-cased handling of PROXY protocol, see `handleProxyCommand` for details: if firstLine { @@ -403,6 +418,11 @@ func (client *Client) tryResume() (success bool) { return } + if oldClient.isTor != client.isTor { + client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection from Tor to non-Tor or vice versa")) + return + } + err := server.clients.Resume(client, oldClient) if err != nil { client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection")) @@ -882,10 +902,10 @@ func (client *Client) destroy(beingResumed bool) { } // remove from connection limits - ipaddr := client.IP() - // this check shouldn't be required but eh - if ipaddr != nil { - client.server.connectionLimiter.RemoveClient(ipaddr) + if client.isTor { + client.server.torLimiter.RemoveClient() + } else { + client.server.connectionLimiter.RemoveClient(client.IP()) } client.server.resumeManager.Delete(client) diff --git a/irc/config.go b/irc/config.go index 7adbe678..2d1cf2f5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -249,6 +249,15 @@ type FakelagConfig struct { Cooldown time.Duration } +type TorListenersConfig struct { + Listeners []string + RequireSasl bool `yaml:"require-sasl"` + Vhost string + MaxConnections int `yaml:"max-connections"` + ThrottleDuration time.Duration `yaml:"throttle-duration"` + MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"` +} + // Config defines the overall configuration. type Config struct { Network struct { @@ -263,6 +272,7 @@ type Config struct { Listen []string UnixBindMode os.FileMode `yaml:"unix-bind-mode"` TLSListeners map[string]*TLSListenConfig `yaml:"tls-listeners"` + TorListeners TorListenersConfig `yaml:"tor-listeners"` STS STSConfig CheckIdent bool `yaml:"check-ident"` MOTD string @@ -694,5 +704,18 @@ func LoadConfig(filename string) (config *Config, err error) { config.History.ClientLength = 0 } + for _, listenAddress := range config.Server.TorListeners.Listeners { + found := false + for _, configuredListener := range config.Server.Listen { + if listenAddress == configuredListener { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("%s is configured as a Tor listener, but is not in server.listen", listenAddress) + } + } + return config, nil } diff --git a/irc/connection_limits/tor.go b/irc/connection_limits/tor.go new file mode 100644 index 00000000..87081465 --- /dev/null +++ b/irc/connection_limits/tor.go @@ -0,0 +1,55 @@ +// Copyright (c) 2019 Shivaram Lingamneni +// released under the MIT license + +package connection_limits + +import ( + "errors" + "sync" + "time" +) + +var ( + ErrLimitExceeded = errors.New("too many concurrent connections") + ErrThrottleExceeded = errors.New("too many recent connection attempts") +) + +// TorLimiter is a combined limiter and throttler for use on connections +// proxied from a Tor hidden service (so we don't have meaningful IPs, +// a notion of CIDR width, etc.) +type TorLimiter struct { + sync.Mutex + + numConnections int + maxConnections int + throttle GenericThrottle +} + +func (tl *TorLimiter) Configure(maxConnections int, duration time.Duration, maxConnectionsPerDuration int) { + tl.Lock() + defer tl.Unlock() + tl.maxConnections = maxConnections + tl.throttle.Duration = duration + tl.throttle.Limit = maxConnectionsPerDuration +} + +func (tl *TorLimiter) AddClient() error { + tl.Lock() + defer tl.Unlock() + + if tl.maxConnections != 0 && tl.maxConnections <= tl.numConnections { + return ErrLimitExceeded + } + throttled, _ := tl.throttle.Touch() + if throttled { + return ErrThrottleExceeded + } + tl.numConnections += 1 + return nil +} + +func (tl *TorLimiter) RemoveClient() { + tl.Lock() + tl.numConnections -= 1 + tl.Unlock() +} diff --git a/irc/gateways.go b/irc/gateways.go index 719ae364..ca42cc79 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -47,6 +47,12 @@ func (wc *webircConfig) Populate() (err error) { // ApplyProxiedIP applies the given IP to the client. func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) { + // PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself + // is whitelisted: + if client.isTor { + return false + } + // ensure IP is sane parsedProxiedIP := net.ParseIP(proxiedIP).To16() if parsedProxiedIP == nil { @@ -61,13 +67,15 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) } // given IP is sane! override the client's current IP - rawHostname := utils.LookupHostname(parsedProxiedIP.String()) + ipstring := parsedProxiedIP.String() + client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring) + rawHostname := utils.LookupHostname(ipstring) + client.stateMutex.Lock() + defer client.stateMutex.Unlock() client.proxiedIP = parsedProxiedIP client.rawHostname = rawHostname - client.stateMutex.Unlock() // nickmask will be updated when the client completes registration - // set tls info client.certfp = "" client.SetMode(modes.TLS, tls) diff --git a/irc/getters.go b/irc/getters.go index 863137b7..96b32309 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -149,6 +149,13 @@ func (client *Client) SetRegistered() { client.stateMutex.Unlock() } +func (client *Client) RawHostname() (result string) { + client.stateMutex.Lock() + result = client.rawHostname + client.stateMutex.Unlock() + return +} + func (client *Client) AwayMessage() (result string) { client.stateMutex.RLock() result = client.awayMessage diff --git a/irc/handlers.go b/irc/handlers.go index 68b7080a..791a75e7 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1878,6 +1878,11 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re targets := strings.Split(msg.Params[0], ",") message := msg.Params[1] + if client.isTor && isRestrictedCTCPMessage(message) { + rb.Add(nil, server.name, "NOTICE", client.t("CTCP messages are disabled over Tor")) + return false + } + // split privmsg splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine)) @@ -1924,7 +1929,9 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re msgid := server.generateMessageID() // restrict messages appropriately when +R is set // intentionally make the sending user think the message went through fine - if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() { + allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() + allowedTor := !user.isTor || !isRestrictedCTCPMessage(message) + if allowedPlusR && allowedTor { user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg) } nickMaskString := client.NickMaskString() @@ -2081,12 +2088,23 @@ func pongHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp return false } +func isRestrictedCTCPMessage(message string) bool { + // block all CTCP privmsgs to Tor clients except for ACTION + // DCC can potentially be used for deanonymization, the others for fingerprinting + return strings.HasPrefix(message, "\x01") && !strings.HasPrefix(message, "\x01ACTION") +} + // PRIVMSG {,} func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { clientOnlyTags := utils.GetClientOnlyTags(msg.Tags) targets := strings.Split(msg.Params[0], ",") message := msg.Params[1] + if client.isTor && isRestrictedCTCPMessage(message) { + rb.Add(nil, server.name, "NOTICE", client.t("CTCP messages are disabled over Tor")) + return false + } + // split privmsg splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine)) @@ -2136,7 +2154,9 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R msgid := server.generateMessageID() // restrict messages appropriately when +R is set // intentionally make the sending user think the message went through fine - if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() { + allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() + allowedTor := !user.isTor || !isRestrictedCTCPMessage(message) + if allowedPlusR && allowedTor { user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) } nickMaskString := client.NickMaskString() diff --git a/irc/resume.go b/irc/resume.go index efb2baa9..9b9b1d05 100644 --- a/irc/resume.go +++ b/irc/resume.go @@ -52,7 +52,8 @@ func (rm *ResumeManager) GenerateToken(client *Client) (token string) { } // VerifyToken looks up the client corresponding to a resume token, returning -// nil if there is no such client or the token is invalid. +// nil if there is no such client or the token is invalid. If successful, +// the token is consumed and cannot be used to resume again. func (rm *ResumeManager) VerifyToken(token string) (client *Client) { if len(token) != 2*utils.SecretTokenLength { return @@ -68,6 +69,8 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) { // disallow resume of an unregistered client; this prevents the use of // resume as an auth bypass if pair.client.Registered() { + // consume the token, ensuring that at most one resume can succeed + delete(rm.resumeIDtoCreds, id) return pair.client } } diff --git a/irc/roleplay.go b/irc/roleplay.go index a2cc376d..2991414d 100644 --- a/irc/roleplay.go +++ b/irc/roleplay.go @@ -19,6 +19,11 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt if isAction { message = fmt.Sprintf("\x01ACTION %s (%s)\x01", message, client.nick) } else { + // block attempts to send CTCP messages to Tor clients + // TODO(#395) clean this up + if len(message) != 0 && message[0] == '\x01' { + return + } message = fmt.Sprintf("%s (%s)", message, client.nick) } diff --git a/irc/server.go b/irc/server.go index 1f2b78f8..8c2c77ff 100644 --- a/irc/server.go +++ b/irc/server.go @@ -21,7 +21,6 @@ import ( "time" "github.com/goshuirc/irc-go/ircfmt" - "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/connection_limits" "github.com/oragono/oragono/irc/isupport" @@ -34,10 +33,7 @@ import ( var ( // common error line to sub values into - errorMsg, _ = (&[]ircmsg.IrcMessage{ircmsg.MakeMessage(nil, "", "ERROR", "%s ")}[0]).Line() - - // common error responses - couldNotParseIPMsg, _ = (&[]ircmsg.IrcMessage{ircmsg.MakeMessage(nil, "", "ERROR", "Unable to parse your IP address")}[0]).Line() + errorMsg = "ERROR :%s\r\n" // supportedUserModesString acts as a cache for when we introduce users supportedUserModesString = modes.SupportedUserModes.String() @@ -57,6 +53,7 @@ var ( type ListenerWrapper struct { listener net.Listener tlsConfig *tls.Config + isTor bool shouldStop bool // protects atomic update of tlsConfig and shouldStop: configMutex sync.Mutex // tier 1 @@ -91,6 +88,7 @@ type Server struct { signals chan os.Signal snomasks *SnoManager store *buntdb.DB + torLimiter connection_limits.TorLimiter whoWas *WhoWasList stats *Stats semaphores *ServerSemaphores @@ -108,6 +106,7 @@ var ( type clientConn struct { Conn net.Conn IsTLS bool + IsTor bool } // NewServer returns a new Oragono server. @@ -245,22 +244,27 @@ func (server *Server) Run() { } func (server *Server) acceptClient(conn clientConn) { - // check IP address - ipaddr := utils.AddrToIP(conn.Conn.RemoteAddr()) - if ipaddr != nil { - isBanned, banMsg := server.checkBans(ipaddr) - if isBanned { - // this might not show up properly on some clients, but our objective here is just to close the connection out before it has a load impact on us - conn.Conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg))) - conn.Conn.Close() - return - } + var isBanned bool + var banMsg string + var ipaddr net.IP + if conn.IsTor { + ipaddr = utils.IPv4LoopbackAddress + isBanned, banMsg = server.checkTorLimits() + } else { + ipaddr = utils.AddrToIP(conn.Conn.RemoteAddr()) + isBanned, banMsg = server.checkBans(ipaddr) + } + + if isBanned { + // this might not show up properly on some clients, but our objective here is just to close the connection out before it has a load impact on us + conn.Conn.Write([]byte(fmt.Sprintf(errorMsg, banMsg))) + conn.Conn.Close() + return } server.logger.Info("localconnect-ip", fmt.Sprintf("Client connecting from %v", ipaddr)) - // prolly don't need to alert snomasks on this, only on connection reg - NewClient(server, conn.Conn, conn.IsTLS) + go RunNewClient(server, conn) } func (server *Server) checkBans(ipaddr net.IP) (banned bool, message string) { @@ -303,12 +307,23 @@ func (server *Server) checkBans(ipaddr net.IP) (banned bool, message string) { return false, "" } +func (server *Server) checkTorLimits() (banned bool, message string) { + switch server.torLimiter.AddClient() { + case connection_limits.ErrLimitExceeded: + return true, "Too many clients from the Tor network" + case connection_limits.ErrThrottleExceeded: + return true, "Exceeded connection throttle for the Tor network" + default: + return false, "" + } +} + // // IRC protocol listeners // // createListener starts a given listener. -func (server *Server) createListener(addr string, tlsConfig *tls.Config, bindMode os.FileMode) (*ListenerWrapper, error) { +func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor bool, bindMode os.FileMode) (*ListenerWrapper, error) { // make listener var listener net.Listener var err error @@ -331,6 +346,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, bindMod wrapper := ListenerWrapper{ listener: listener, tlsConfig: tlsConfig, + isTor: isTor, shouldStop: false, } @@ -342,10 +358,10 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, bindMod conn, err := listener.Accept() // synchronously access config data: - // whether TLS is enabled and whether we should stop listening wrapper.configMutex.Lock() shouldStop = wrapper.shouldStop tlsConfig = wrapper.tlsConfig + isTor = wrapper.isTor wrapper.configMutex.Unlock() if err == nil { @@ -355,6 +371,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, bindMod newConn := clientConn{ Conn: conn, IsTLS: tlsConfig != nil, + IsTor: isTor, } // hand off the connection go server.acceptClient(newConn) @@ -515,7 +532,7 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, cnick, tnick, tOper.WhoisLine) } if client.HasMode(modes.Operator) || client == target { - rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, cnick, tnick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP")) + rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, cnick, tnick, fmt.Sprintf("%s@%s", targetInfo.username, target.RawHostname()), target.IPString(), client.t("Actual user@host, Actual IP")) } if target.HasMode(modes.TLS) { rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection")) @@ -631,6 +648,9 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { return err } + tlConf := &config.Server.TorListeners + server.torLimiter.Configure(tlConf.MaxConnections, tlConf.ThrottleDuration, tlConf.MaxConnectionsPerDuration) + // reload logging config wasLoggingRawIO := !initial && server.logger.IsLoggingRawIO() err = server.logger.ApplyConfig(config.Logging) @@ -908,9 +928,9 @@ func (server *Server) loadDatastore(config *Config) error { } func (server *Server) setupListeners(config *Config) (err error) { - logListener := func(addr string, tlsconfig *tls.Config) { + logListener := func(addr string, tlsconfig *tls.Config, isTor bool) { server.logger.Info("listeners", - fmt.Sprintf("now listening on %s, tls=%t.", addr, (tlsconfig != nil)), + fmt.Sprintf("now listening on %s, tls=%t, tor=%t.", addr, (tlsconfig != nil), isTor), ) } @@ -920,6 +940,15 @@ func (server *Server) setupListeners(config *Config) (err error) { return } + isTorListener := func(listener string) bool { + for _, torListener := range config.Server.TorListeners.Listeners { + if listener == torListener { + return true + } + } + return false + } + // update or destroy all existing listeners for addr := range server.listeners { currentListener := server.listeners[addr] @@ -935,13 +964,16 @@ func (server *Server) setupListeners(config *Config) (err error) { // its next Accept(). this is like sending over a buffered channel of // size 1, but where sending a second item overwrites the buffered item // instead of blocking. + tlsConfig := tlsListeners[addr] + isTor := isTorListener(addr) currentListener.configMutex.Lock() currentListener.shouldStop = !stillConfigured - currentListener.tlsConfig = tlsListeners[addr] + currentListener.tlsConfig = tlsConfig + currentListener.isTor = isTor currentListener.configMutex.Unlock() if stillConfigured { - logListener(addr, currentListener.tlsConfig) + logListener(addr, tlsConfig, isTor) } else { // tell the listener it should stop by interrupting its Accept() call: currentListener.listener.Close() @@ -955,15 +987,16 @@ func (server *Server) setupListeners(config *Config) (err error) { _, exists := server.listeners[newaddr] if !exists { // make new listener + isTor := isTorListener(newaddr) tlsConfig := tlsListeners[newaddr] - listener, listenerErr := server.createListener(newaddr, tlsConfig, config.Server.UnixBindMode) + listener, listenerErr := server.createListener(newaddr, tlsConfig, isTor, config.Server.UnixBindMode) if listenerErr != nil { server.logger.Error("server", "couldn't listen on", newaddr, listenerErr.Error()) err = listenerErr continue } server.listeners[newaddr] = listener - logListener(newaddr, tlsConfig) + logListener(newaddr, tlsConfig, isTor) } } diff --git a/irc/utils/crypto.go b/irc/utils/crypto.go index d332a355..116f596a 100644 --- a/irc/utils/crypto.go +++ b/irc/utils/crypto.go @@ -10,8 +10,8 @@ import ( ) var ( - // standard b32 alphabet, but in lowercase for silly aesthetic reasons - b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding) + // slingamn's own private b32 alphabet, removing 1, l, o, and 0 + b32encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding) ) const ( diff --git a/oragono.yaml b/oragono.yaml index ebc85c65..3ee3773a 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -33,6 +33,30 @@ server: key: tls.key cert: tls.crt + # tor listeners: designate listeners for use by a tor hidden service / .onion address + # WARNING: if you are running oragono as a pure hidden service, see the + # anonymization / hardening recommendations in docs/MANUAL.md + tor-listeners: + # any connections that come in on these listeners will be considered + # Tor connections. it is strongly recommended that these listeners *not* + # be on public interfaces: they should be on 127.0.0.0/8 or unix domain + listeners: + # - "/tmp/oragono_tor_sock" + + # if this is true, connections from Tor must authenticate with SASL + require-sasl: false + + # what hostname should be displayed for Tor connections? + vhost: "tor-network.onion" + + # allow at most this many connections at once (0 for no limit): + max-connections: 64 + + # connection throttling (limit how many connection attempts are allowed at once): + throttle-duration: 10m + # set to 0 to disable throttling: + max-connections-per-duration: 64 + # strict transport security, to get clients to automagically use TLS sts: # whether to advertise STS