From 0d2cf21cec7290213fed81cfd3fafcac82819baa Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 5 Feb 2019 13:44:33 -0500 Subject: [PATCH 01/10] clean something up in ApplyProxiedIP --- irc/gateways.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/irc/gateways.go b/irc/gateways.go index 719ae364..f40fdf71 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -61,13 +61,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) From f790a910cd75f640f81f2ae1e6df98d6a57b6e14 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 7 Feb 2019 20:41:25 -0500 Subject: [PATCH 02/10] change the b32 alphabet for absolutely no reason --- irc/utils/crypto.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ( From 7018e3693bfc545342922e16e6051ac36857b9bd Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 28 Oct 2018 15:44:13 -0400 Subject: [PATCH 03/10] optimization: check IsLoggingRawIO before attempting to log input --- irc/client.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/irc/client.go b/irc/client.go index af91f03e..a4f76339 100644 --- a/irc/client.go +++ b/irc/client.go @@ -315,7 +315,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 { From d43ce07b669e496f3ff84dd8954968233f306402 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 19 Feb 2019 16:47:04 -0500 Subject: [PATCH 04/10] consume resume token during VerifyToken Independently of this, ClientLookupSet.Resume ensures that at most one resume can succeed, so this doesn't actually change the behavior. But ResumeManager should be a standalone example of how to implement resume without race conditions. --- irc/resume.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 } } From b0f89062fab24c727fe961d3d8b6125d63062c1c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 25 Feb 2019 21:50:43 -0500 Subject: [PATCH 05/10] add support for tor (#369) --- docs/MANUAL.md | 44 ++++++++++++++ irc/client.go | 111 +++++++++++++++++++---------------- irc/config.go | 23 ++++++++ irc/connection_limits/tor.go | 55 +++++++++++++++++ irc/gateways.go | 6 ++ irc/getters.go | 7 +++ irc/handlers.go | 24 +++++++- irc/roleplay.go | 5 ++ irc/server.go | 85 +++++++++++++++++++-------- oragono.yaml | 24 ++++++++ 10 files changed, 307 insertions(+), 77 deletions(-) create mode 100644 irc/connection_limits/tor.go diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 5d9c957e..a05666ba 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -34,6 +34,8 @@ _Copyright © 2018 Daniel Oaks _ - Commands - Integrating with other software - HOPM + - ZNC + - Tor - Acknowledgements @@ -601,6 +603,48 @@ 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` 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 +```` + +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, Oragono must be configured to listen on a Unix domain socket that the Tor daemon can connect to. However, distributions typically package Tor with its own hardening profiles, which will restrict which sockets it can connect to. 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 a4f76339..2752ea76 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, loginThrottle: connection_limits.GenericThrottle{ Duration: config.Accounts.LoginThrottling.Duration, Limit: config.Accounts.LoginThrottling.MaxAttempts, @@ -145,58 +147,73 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) { } client.languages = server.languages.Default() - 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 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 { @@ -884,10 +897,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 112a5387..4fd71faa 100644 --- a/irc/config.go +++ b/irc/config.go @@ -251,6 +251,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 { @@ -265,6 +274,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 @@ -806,5 +816,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 f40fdf71..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 { diff --git a/irc/getters.go b/irc/getters.go index 1f6ff9cb..6fef1fd8 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -143,6 +143,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 dfbf0ac5..ce3fb292 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1884,6 +1884,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)) @@ -1930,7 +1935,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() @@ -2087,12 +2094,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)) @@ -2142,7 +2160,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/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 fdf4f01e..23b71614 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" @@ -35,10 +34,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() @@ -58,6 +54,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 @@ -92,6 +89,7 @@ type Server struct { signals chan os.Signal snomasks *SnoManager store *buntdb.DB + torLimiter connection_limits.TorLimiter whoWas *WhoWasList stats *Stats semaphores *ServerSemaphores @@ -109,6 +107,7 @@ var ( type clientConn struct { Conn net.Conn IsTLS bool + IsTor bool } // NewServer returns a new Oragono server. @@ -252,22 +251,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) { @@ -310,12 +314,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 @@ -338,6 +353,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, bindMod wrapper := ListenerWrapper{ listener: listener, tlsConfig: tlsConfig, + isTor: isTor, shouldStop: false, } @@ -349,10 +365,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 { @@ -362,6 +378,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) @@ -524,7 +541,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")) @@ -639,6 +656,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) @@ -931,9 +951,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), ) } @@ -943,6 +963,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] @@ -958,13 +987,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() @@ -978,15 +1010,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/oragono.yaml b/oragono.yaml index 8181f729..a1a9790f 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 From d13f58acf02220ceec8521cfd2c9c5d9111918e4 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 25 Feb 2019 22:56:08 -0500 Subject: [PATCH 06/10] review fixes --- irc/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irc/client.go b/irc/client.go index 2752ea76..898feadc 100644 --- a/irc/client.go +++ b/irc/client.go @@ -205,11 +205,11 @@ func (client *Client) doIdentLookup(conn net.Conn) { func (client *Client) isAuthorized(config *Config) bool { saslSent := client.account != "" // PASS requirement - if !((config.Server.passwordBytes == nil) || client.sentPassCommand || (config.Accounts.SkipServerPassword && saslSent)) { + if (config.Server.passwordBytes != nil) && !client.sentPassCommand && !(config.Accounts.SkipServerPassword && saslSent) { return false } // Tor connections may be required to authenticate with SASL - if config.Server.TorListeners.RequireSasl && !saslSent { + if client.isTor && config.Server.TorListeners.RequireSasl && !saslSent { return false } // finally, enforce require-sasl From 1ecd9744194bf741cdd3841d2194c80cda556a33 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 25 Feb 2019 22:59:38 -0500 Subject: [PATCH 07/10] punctuation fix in manual --- docs/MANUAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/MANUAL.md b/docs/MANUAL.md index a05666ba..8366aa03 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -607,7 +607,7 @@ Versions of ZNC prior to 1.7 have a [bug](https://github.com/znc/znc/issues/1212 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` Configure Tor like this: +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 From 7ceaae426c995246d68d89da53a22269dbd3ed03 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 26 Feb 2019 14:44:30 -0500 Subject: [PATCH 08/10] manual tweaks --- docs/MANUAL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 8366aa03..a95ccee8 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -32,7 +32,7 @@ _Copyright © 2018 Daniel Oaks _ - Channel Modes - Channel Prefixes - Commands -- Integrating with other software +- Working with other software - HOPM - ZNC - Tor @@ -543,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. @@ -625,7 +625,7 @@ The second way is to run Oragono as a true hidden service, where the server's ac * 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, Oragono must be configured to listen on a Unix domain socket that the Tor daemon can connect to. However, distributions typically package Tor with its own hardening profiles, which will restrict which sockets it can connect to. Below is a recipe for configuring this with the official Tor packages for Debian: + * 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`. From 18169cbedf5385d36e3b2863b186c485bc167b6b Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 26 Feb 2019 16:39:10 -0500 Subject: [PATCH 09/10] disallow resume from tor to non-tor --- irc/client.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/irc/client.go b/irc/client.go index 843c3cf0..3ae9ac22 100644 --- a/irc/client.go +++ b/irc/client.go @@ -418,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")) From 63502b8da4328bee3b168d6d690318f45ac31f58 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 26 Feb 2019 21:00:35 -0500 Subject: [PATCH 10/10] add a note about tor vs. tls --- docs/MANUAL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/MANUAL.md b/docs/MANUAL.md index a95ccee8..1422dd1e 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -619,6 +619,8 @@ 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`.