Merge pull request #481 from slingamn/cloaks.5

implement ip cloaking
This commit is contained in:
Shivaram Lingamneni 2019-05-12 20:23:45 -04:00 committed by GitHub
commit 13dda00989
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 264 additions and 4 deletions

@ -20,6 +20,7 @@ test:
python3 ./gencapdefs.py | diff - ${capdef_file}
cd irc && go test . && go vet .
cd irc/caps && go test . && go vet .
cd irc/cloaks && go test . && go vet .
cd irc/connection_limits && go test . && go vet .
cd irc/history && go test . && go vet .
cd irc/isupport && go test . && go vet .

@ -26,6 +26,7 @@ _Copyright © 2018 Daniel Oaks <daniel@danieloaks.net>_
- Nickname reservation
- Channel Registration
- Language
- IP cloaking
- Frequently Asked Questions
- Modes
- User Modes
@ -242,6 +243,16 @@ The above will change the server language to Romanian, with a fallback to Chines
Our language and translation functionality is very early, so feel free to let us know if there are any troubles with it! If you know another language and you'd like to contribute, we've got a CrowdIn project here: [https://crowdin.com/project/oragono](https://crowdin.com/project/oragono)
## IP cloaking
Unlike many other chat and web platforms, IRC traditionally exposes the user's IP and hostname information to other users. This is in part because channel owners and operators (who have privileges over a single channel, but not over the server as a whole) need to be able to ban spammers and abusers from their channels, including via hostnames in cases where the abuser tries to evade the ban.
IP cloaking is a way of balancing these concerns about abuse with concerns about user privacy. With cloaking, the user's IP address is deterministically "scrambled", typically via a cryptographic [MAC](https://en.wikipedia.org/wiki/Message_authentication_code), to form a "cloaked" hostname that replaces the usual reverse-DNS-based hostname. Users cannot reverse the scrambling to learn each other's IPs, but can ban a scrambled address the same way they would ban a regular hostname.
Oragono supports cloaking, which can be enabled via the `server.ip-cloaking` section of the config. However, Oragono's cloaking behavior differs from other IRC software. Rather than scrambling each of the 4 bytes of the IPv4 address (or each 2-byte pair of the 8 such pairs of the IPv6 address) separately, the server administrator configures a CIDR length (essentially, a fixed number of most-significant-bits of the address). The CIDR (i.e., only the most significant portion of the address) is then scrambled atomically to produce the cloaked hostname. This errs on the side of user privacy, since knowing the cloaked hostname for one CIDR tells you nothing about the cloaked hostnames of other CIDRs --- the scheme reveals only whether two users are coming from the same CIDR. We suggest using 32-bit CIDRs for IPv4 (i.e., the whole address) and 64-bit CIDRs for IPv6, since these are the typical assignments made by ISPs to individual customers.
Setting `server.ip-cloaking.num-bits` to 0 gives users cloaks that don't depend on their IP address information at all, which is an option for deployments where privacy is a more pressing concern than abuse. Holders of registered accounts can also use the vhost system (for details, `/msg HostServ HELP`.)
-------------------------------------------------------------------------------------------

@ -70,6 +70,7 @@ type Client struct {
preregNick string
proxiedIP net.IP // actual remote IP if using the PROXY protocol
rawHostname string
cloakedHostname string
realname string
realIP net.IP
registered bool
@ -215,6 +216,7 @@ func RunNewClient(server *Server, conn clientConn) {
session.realIP = utils.AddrToIP(remoteAddr)
// 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)
if utils.AddrIsLocal(remoteAddr) {
// treat local connections as secure (may be overridden later by WEBIRC)
client.SetMode(modes.TLS, true)
@ -812,7 +814,10 @@ func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
func (client *Client) updateNickMaskNoMutex() {
client.hostname = client.getVHostNoMutex()
if client.hostname == "" {
client.hostname = client.rawHostname
client.hostname = client.cloakedHostname
if client.hostname == "" {
client.hostname = client.rawHostname
}
}
cfhostname, err := Casefold(client.hostname)
@ -831,6 +836,7 @@ func (client *Client) AllNickmasks() (masks []string) {
nick := client.nickCasefolded
username := client.username
rawHostname := client.rawHostname
cloakedHostname := client.cloakedHostname
vhost := client.getVHostNoMutex()
client.stateMutex.RUnlock()
username = strings.ToLower(username)
@ -849,6 +855,10 @@ func (client *Client) AllNickmasks() (masks []string) {
masks = append(masks, rawhostmask)
}
if cloakedHostname != "" {
masks = append(masks, fmt.Sprintf("%s!%s@%s", nick, username, cloakedHostname))
}
ipmask := fmt.Sprintf("%s!%s@%s", nick, username, client.IPString())
if ipmask != rawhostmask {
masks = append(masks, ipmask)

106
irc/cloaks/cloak_test.go Normal file

@ -0,0 +1,106 @@
// Copyright (c) 2019 Shivaram Lingamneni
// released under the MIT license
package cloaks
import (
"net"
"reflect"
"testing"
)
func assertEqual(supplied, expected interface{}, t *testing.T) {
if !reflect.DeepEqual(supplied, expected) {
t.Errorf("expected %v but got %v", expected, supplied)
}
}
func easyParseIP(ipstr string) (result net.IP) {
result = net.ParseIP(ipstr)
if result == nil {
panic(ipstr)
}
return
}
func cloakConfForTesting() CloakConfig {
config := CloakConfig{
Enabled: true,
Netname: "oragono",
Secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
CidrLenIPv4: 32,
CidrLenIPv6: 64,
NumBits: 80,
}
config.Initialize()
return config
}
func TestCloakDeterminism(t *testing.T) {
config := cloakConfForTesting()
v4ip := easyParseIP("8.8.8.8").To4()
assertEqual(config.ComputeCloak(v4ip), "d2z5guriqhzwazyr.oragono", t)
// use of the 4-in-6 mapping should not affect the cloak
v6mappedIP := v4ip.To16()
assertEqual(config.ComputeCloak(v6mappedIP), "d2z5guriqhzwazyr.oragono", t)
v6ip := easyParseIP("2001:0db8::1")
assertEqual(config.ComputeCloak(v6ip), "w7ren6nxii6f3i3d.oragono", t)
// same CIDR, so same cloak:
v6ipsamecidr := easyParseIP("2001:0db8::2")
assertEqual(config.ComputeCloak(v6ipsamecidr), "w7ren6nxii6f3i3d.oragono", t)
v6ipdifferentcidr := easyParseIP("2001:0db9::1")
// different CIDR, different cloak:
assertEqual(config.ComputeCloak(v6ipdifferentcidr), "ccmptyrjwsxv4f4d.oragono", t)
// cloak values must be sensitive to changes in the secret key
config.Secret = "HJcXK4lLawxBE4-9SIdPji_21YiL3N5r5f5-SPNrGVY"
assertEqual(config.ComputeCloak(v4ip), "4khy3usk8mfu42pe.oragono", t)
assertEqual(config.ComputeCloak(v6mappedIP), "4khy3usk8mfu42pe.oragono", t)
assertEqual(config.ComputeCloak(v6ip), "mxpk3c83vdxkek9j.oragono", t)
assertEqual(config.ComputeCloak(v6ipsamecidr), "mxpk3c83vdxkek9j.oragono", t)
}
func TestCloakShortv4Cidr(t *testing.T) {
config := CloakConfig{
Enabled: true,
Netname: "oragono",
Secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
CidrLenIPv4: 24,
CidrLenIPv6: 64,
NumBits: 60,
}
config.Initialize()
v4ip := easyParseIP("8.8.8.8")
assertEqual(config.ComputeCloak(v4ip), "3cay3zc72tnui.oragono", t)
v4ipsamecidr := easyParseIP("8.8.8.9")
assertEqual(config.ComputeCloak(v4ipsamecidr), "3cay3zc72tnui.oragono", t)
}
func TestCloakZeroBits(t *testing.T) {
config := cloakConfForTesting()
config.NumBits = 0
config.Netname = "example.com"
config.Initialize()
v4ip := easyParseIP("8.8.8.8").To4()
assertEqual(config.ComputeCloak(v4ip), "example.com", t)
}
func TestCloakDisabled(t *testing.T) {
config := cloakConfForTesting()
config.Enabled = false
v4ip := easyParseIP("8.8.8.8").To4()
assertEqual(config.ComputeCloak(v4ip), "", t)
}
func BenchmarkCloaks(b *testing.B) {
config := cloakConfForTesting()
v6ip := easyParseIP("2001:0db8::1")
b.ResetTimer()
for i := 0; i < b.N; i++ {
config.ComputeCloak(v6ip)
}
}

70
irc/cloaks/cloaks.go Normal file

@ -0,0 +1,70 @@
// Copyright (c) 2019 Shivaram Lingamneni
package cloaks
import (
"fmt"
"net"
"golang.org/x/crypto/sha3"
"github.com/oragono/oragono/irc/utils"
)
type CloakConfig struct {
Enabled bool
Netname string
Secret string
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
NumBits int `yaml:"num-bits"`
numBytes int
ipv4Mask net.IPMask
ipv6Mask net.IPMask
}
func (cloakConfig *CloakConfig) Initialize() {
// sanity checks:
numBits := cloakConfig.NumBits
if 0 == numBits {
numBits = 80
} else if 256 < numBits {
numBits = 256
}
// derived values:
cloakConfig.numBytes = numBits / 8
// round up to the nearest byte
if numBits%8 != 0 {
cloakConfig.numBytes += 1
}
cloakConfig.ipv4Mask = net.CIDRMask(cloakConfig.CidrLenIPv4, 32)
cloakConfig.ipv6Mask = net.CIDRMask(cloakConfig.CidrLenIPv6, 128)
}
// simple cloaking algorithm: normalize the IP to its CIDR,
// then hash the resulting bytes with a secret key,
// then truncate to the desired length, b32encode, and append the fake TLD.
func (config *CloakConfig) ComputeCloak(ip net.IP) string {
if !config.Enabled {
return ""
} else if config.NumBits == 0 {
return config.Netname
}
var masked net.IP
v4ip := ip.To4()
if v4ip != nil {
masked = v4ip.Mask(config.ipv4Mask)
} else {
masked = ip.Mask(config.ipv6Mask)
}
// SHA3(K || M):
// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
input := make([]byte, len(config.Secret)+len(masked))
copy(input, config.Secret[:])
copy(input[len(config.Secret):], masked)
digest := sha3.Sum512(input)
b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
return fmt.Sprintf("%s.%s", b32digest, config.Netname)
}

@ -18,6 +18,7 @@ import (
"time"
"code.cloudfoundry.org/bytefmt"
"github.com/oragono/oragono/irc/cloaks"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/custime"
"github.com/oragono/oragono/irc/isupport"
@ -297,6 +298,7 @@ type Config struct {
isupport isupport.List
ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"`
ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
}
Languages struct {
@ -728,6 +730,13 @@ func LoadConfig(filename string) (config *Config, err error) {
config.History.ClientLength = 0
}
config.Server.Cloaks.Initialize()
if config.Server.Cloaks.Enabled {
if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
return nil, fmt.Errorf("You must generate a new value of server.ip-cloaking.secret to enable cloaking")
}
}
for _, listenAddress := range config.Server.TorListeners.Listeners {
found := false
for _, configuredListener := range config.Server.Listen {

@ -70,6 +70,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
ipstring := 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()
defer client.stateMutex.Unlock()
@ -77,6 +78,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
client.proxiedIP = parsedProxiedIP
session.rawHostname = rawHostname
client.rawHostname = rawHostname
client.cloakedHostname = cloakedHostname
// nickmask will be updated when the client completes registration
// set tls info
client.certfp = ""

@ -7,11 +7,12 @@ import (
"crypto/rand"
"crypto/subtle"
"encoding/base32"
"encoding/base64"
)
var (
// slingamn's own private b32 alphabet, removing 1, l, o, and 0
b32encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
)
const (
@ -24,7 +25,7 @@ func GenerateSecretToken() string {
var buf [16]byte
rand.Read(buf[:])
// 26 ASCII characters, should be fine for most purposes
return b32encoder.EncodeToString(buf[:])
return B32Encoder.EncodeToString(buf[:])
}
// securely check if a supplied token matches a stored token
@ -37,3 +38,10 @@ func SecretTokensMatch(storedToken string, suppliedToken string) bool {
return subtle.ConstantTimeCompare([]byte(storedToken), []byte(suppliedToken)) == 1
}
// generate a 256-bit secret key that can be written into a config file
func GenerateSecretKey() string {
var buf [32]byte
rand.Read(buf[:])
return base64.RawURLEncoding.EncodeToString(buf[:])
}

@ -17,6 +17,7 @@ import (
"github.com/oragono/oragono/irc"
"github.com/oragono/oragono/irc/logger"
"github.com/oragono/oragono/irc/mkcerts"
"github.com/oragono/oragono/irc/utils"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh/terminal"
)
@ -46,6 +47,7 @@ Usage:
oragono upgradedb [--conf <filename>] [--quiet]
oragono genpasswd [--conf <filename>] [--quiet]
oragono mkcerts [--conf <filename>] [--quiet]
oragono mksecret [--conf <filename>] [--quiet]
oragono run [--conf <filename>] [--quiet]
oragono -h | --help
oragono --version
@ -57,7 +59,7 @@ Options:
arguments, _ := docopt.ParseArgs(usage, nil, version)
// don't require a config file for genpasswd
// don't require a config file for genpasswd or mksecret
if arguments["genpasswd"].(bool) {
var password string
fd := int(os.Stdin.Fd())
@ -83,6 +85,9 @@ Options:
fmt.Println()
}
return
} else if arguments["mksecret"].(bool) {
fmt.Println(utils.GenerateSecretKey())
return
}
configfile := arguments["--conf"].(string)

@ -188,6 +188,44 @@ server:
# - "192.168.1.1"
# - "2001:0db8::/32"
# IP cloaking hides users' IP addresses from other users and from channel admins
# (but not from server admins), while still allowing channel admins to ban
# offending IP addresses or networks. In place of hostnames derived from reverse
# DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are
# generated deterministically from the underlying IP address, but if the underlying
# IP is not already known, it is infeasible to recover it from the cloaked name.
ip-cloaking:
# whether to enable IP cloaking
enabled: false
# fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.oragono
netname: "oragono"
# secret key to prevent dictionary attacks against cloaked IPs
# any high-entropy secret is valid for this purpose:
# you MUST generate a new one for your installation.
# suggestion: use the output of `oragono mksecret`
# note that rotating this key will invalidate all existing ban masks.
secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4"
# the cloaked hostname is derived only from the CIDR (most significant bits
# of the IP address), up to a configurable number of bits. this is the
# granularity at which bans will take effect for ipv4 (a /32 is a fully
# specified IP address). note that changing this value will invalidate
# any stored bans.
cidr-len-ipv4: 32
# analogous value for ipv6 (an ipv6 /64 is the typical prefix assigned
# by an ISP to an individual customer for their LAN)
cidr-len-ipv6: 64
# number of bits of hash output to include in the cloaked hostname.
# more bits means less likelihood of distinct IPs colliding,
# at the cost of a longer cloaked hostname. if this value is set to 0,
# all users will receive simply `netname` as their cloaked hostname.
num-bits: 80
# account options
accounts:
# account registration