Browse Source

Merge pull request #481 from slingamn/cloaks.5

implement ip cloaking
tags/v1.1.0-rc1
Shivaram Lingamneni 5 years ago
parent
commit
13dda00989
No account linked to committer's email address
10 changed files with 264 additions and 4 deletions
  1. 1
    0
      Makefile
  2. 11
    0
      docs/MANUAL.md
  3. 11
    1
      irc/client.go
  4. 106
    0
      irc/cloaks/cloak_test.go
  5. 70
    0
      irc/cloaks/cloaks.go
  6. 9
    0
      irc/config.go
  7. 2
    0
      irc/gateways.go
  8. 10
    2
      irc/utils/crypto.go
  9. 6
    1
      oragono.go
  10. 38
    0
      oragono.yaml

+ 1
- 0
Makefile View File

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

+ 11
- 0
docs/MANUAL.md View File

@@ -26,6 +26,7 @@ _Copyright © 2018 Daniel Oaks <daniel@danieloaks.net>_
26 26
         - Nickname reservation
27 27
     - Channel Registration
28 28
     - Language
29
+    - IP cloaking
29 30
 - Frequently Asked Questions
30 31
 - Modes
31 32
     - User Modes
@@ -242,6 +243,16 @@ The above will change the server language to Romanian, with a fallback to Chines
242 243
 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)
243 244
 
244 245
 
246
+## IP cloaking
247
+
248
+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.
249
+
250
+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.
251
+
252
+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.
253
+
254
+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`.)
255
+
245 256
 -------------------------------------------------------------------------------------------
246 257
 
247 258
 

+ 11
- 1
irc/client.go View File

@@ -70,6 +70,7 @@ type Client struct {
70 70
 	preregNick         string
71 71
 	proxiedIP          net.IP // actual remote IP if using the PROXY protocol
72 72
 	rawHostname        string
73
+	cloakedHostname    string
73 74
 	realname           string
74 75
 	realIP             net.IP
75 76
 	registered         bool
@@ -215,6 +216,7 @@ func RunNewClient(server *Server, conn clientConn) {
215 216
 		session.realIP = utils.AddrToIP(remoteAddr)
216 217
 		// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
217 218
 		session.rawHostname = utils.LookupHostname(session.realIP.String())
219
+		client.cloakedHostname = config.Server.Cloaks.ComputeCloak(session.realIP)
218 220
 		if utils.AddrIsLocal(remoteAddr) {
219 221
 			// treat local connections as secure (may be overridden later by WEBIRC)
220 222
 			client.SetMode(modes.TLS, true)
@@ -812,7 +814,10 @@ func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
812 814
 func (client *Client) updateNickMaskNoMutex() {
813 815
 	client.hostname = client.getVHostNoMutex()
814 816
 	if client.hostname == "" {
815
-		client.hostname = client.rawHostname
817
+		client.hostname = client.cloakedHostname
818
+		if client.hostname == "" {
819
+			client.hostname = client.rawHostname
820
+		}
816 821
 	}
817 822
 
818 823
 	cfhostname, err := Casefold(client.hostname)
@@ -831,6 +836,7 @@ func (client *Client) AllNickmasks() (masks []string) {
831 836
 	nick := client.nickCasefolded
832 837
 	username := client.username
833 838
 	rawHostname := client.rawHostname
839
+	cloakedHostname := client.cloakedHostname
834 840
 	vhost := client.getVHostNoMutex()
835 841
 	client.stateMutex.RUnlock()
836 842
 	username = strings.ToLower(username)
@@ -849,6 +855,10 @@ func (client *Client) AllNickmasks() (masks []string) {
849 855
 		masks = append(masks, rawhostmask)
850 856
 	}
851 857
 
858
+	if cloakedHostname != "" {
859
+		masks = append(masks, fmt.Sprintf("%s!%s@%s", nick, username, cloakedHostname))
860
+	}
861
+
852 862
 	ipmask := fmt.Sprintf("%s!%s@%s", nick, username, client.IPString())
853 863
 	if ipmask != rawhostmask {
854 864
 		masks = append(masks, ipmask)

+ 106
- 0
irc/cloaks/cloak_test.go View File

@@ -0,0 +1,106 @@
1
+// Copyright (c) 2019 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package cloaks
5
+
6
+import (
7
+	"net"
8
+	"reflect"
9
+	"testing"
10
+)
11
+
12
+func assertEqual(supplied, expected interface{}, t *testing.T) {
13
+	if !reflect.DeepEqual(supplied, expected) {
14
+		t.Errorf("expected %v but got %v", expected, supplied)
15
+	}
16
+}
17
+
18
+func easyParseIP(ipstr string) (result net.IP) {
19
+	result = net.ParseIP(ipstr)
20
+	if result == nil {
21
+		panic(ipstr)
22
+	}
23
+	return
24
+}
25
+
26
+func cloakConfForTesting() CloakConfig {
27
+	config := CloakConfig{
28
+		Enabled:     true,
29
+		Netname:     "oragono",
30
+		Secret:      "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
31
+		CidrLenIPv4: 32,
32
+		CidrLenIPv6: 64,
33
+		NumBits:     80,
34
+	}
35
+	config.Initialize()
36
+	return config
37
+}
38
+
39
+func TestCloakDeterminism(t *testing.T) {
40
+	config := cloakConfForTesting()
41
+
42
+	v4ip := easyParseIP("8.8.8.8").To4()
43
+	assertEqual(config.ComputeCloak(v4ip), "d2z5guriqhzwazyr.oragono", t)
44
+	// use of the 4-in-6 mapping should not affect the cloak
45
+	v6mappedIP := v4ip.To16()
46
+	assertEqual(config.ComputeCloak(v6mappedIP), "d2z5guriqhzwazyr.oragono", t)
47
+
48
+	v6ip := easyParseIP("2001:0db8::1")
49
+	assertEqual(config.ComputeCloak(v6ip), "w7ren6nxii6f3i3d.oragono", t)
50
+	// same CIDR, so same cloak:
51
+	v6ipsamecidr := easyParseIP("2001:0db8::2")
52
+	assertEqual(config.ComputeCloak(v6ipsamecidr), "w7ren6nxii6f3i3d.oragono", t)
53
+	v6ipdifferentcidr := easyParseIP("2001:0db9::1")
54
+	// different CIDR, different cloak:
55
+	assertEqual(config.ComputeCloak(v6ipdifferentcidr), "ccmptyrjwsxv4f4d.oragono", t)
56
+
57
+	// cloak values must be sensitive to changes in the secret key
58
+	config.Secret = "HJcXK4lLawxBE4-9SIdPji_21YiL3N5r5f5-SPNrGVY"
59
+	assertEqual(config.ComputeCloak(v4ip), "4khy3usk8mfu42pe.oragono", t)
60
+	assertEqual(config.ComputeCloak(v6mappedIP), "4khy3usk8mfu42pe.oragono", t)
61
+	assertEqual(config.ComputeCloak(v6ip), "mxpk3c83vdxkek9j.oragono", t)
62
+	assertEqual(config.ComputeCloak(v6ipsamecidr), "mxpk3c83vdxkek9j.oragono", t)
63
+}
64
+
65
+func TestCloakShortv4Cidr(t *testing.T) {
66
+	config := CloakConfig{
67
+		Enabled:     true,
68
+		Netname:     "oragono",
69
+		Secret:      "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
70
+		CidrLenIPv4: 24,
71
+		CidrLenIPv6: 64,
72
+		NumBits:     60,
73
+	}
74
+	config.Initialize()
75
+
76
+	v4ip := easyParseIP("8.8.8.8")
77
+	assertEqual(config.ComputeCloak(v4ip), "3cay3zc72tnui.oragono", t)
78
+	v4ipsamecidr := easyParseIP("8.8.8.9")
79
+	assertEqual(config.ComputeCloak(v4ipsamecidr), "3cay3zc72tnui.oragono", t)
80
+}
81
+
82
+func TestCloakZeroBits(t *testing.T) {
83
+	config := cloakConfForTesting()
84
+	config.NumBits = 0
85
+	config.Netname = "example.com"
86
+	config.Initialize()
87
+
88
+	v4ip := easyParseIP("8.8.8.8").To4()
89
+	assertEqual(config.ComputeCloak(v4ip), "example.com", t)
90
+}
91
+
92
+func TestCloakDisabled(t *testing.T) {
93
+	config := cloakConfForTesting()
94
+	config.Enabled = false
95
+	v4ip := easyParseIP("8.8.8.8").To4()
96
+	assertEqual(config.ComputeCloak(v4ip), "", t)
97
+}
98
+
99
+func BenchmarkCloaks(b *testing.B) {
100
+	config := cloakConfForTesting()
101
+	v6ip := easyParseIP("2001:0db8::1")
102
+	b.ResetTimer()
103
+	for i := 0; i < b.N; i++ {
104
+		config.ComputeCloak(v6ip)
105
+	}
106
+}

+ 70
- 0
irc/cloaks/cloaks.go View File

@@ -0,0 +1,70 @@
1
+// Copyright (c) 2019 Shivaram Lingamneni
2
+
3
+package cloaks
4
+
5
+import (
6
+	"fmt"
7
+	"net"
8
+
9
+	"golang.org/x/crypto/sha3"
10
+
11
+	"github.com/oragono/oragono/irc/utils"
12
+)
13
+
14
+type CloakConfig struct {
15
+	Enabled     bool
16
+	Netname     string
17
+	Secret      string
18
+	CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
19
+	CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
20
+	NumBits     int `yaml:"num-bits"`
21
+
22
+	numBytes int
23
+	ipv4Mask net.IPMask
24
+	ipv6Mask net.IPMask
25
+}
26
+
27
+func (cloakConfig *CloakConfig) Initialize() {
28
+	// sanity checks:
29
+	numBits := cloakConfig.NumBits
30
+	if 0 == numBits {
31
+		numBits = 80
32
+	} else if 256 < numBits {
33
+		numBits = 256
34
+	}
35
+
36
+	// derived values:
37
+	cloakConfig.numBytes = numBits / 8
38
+	// round up to the nearest byte
39
+	if numBits%8 != 0 {
40
+		cloakConfig.numBytes += 1
41
+	}
42
+	cloakConfig.ipv4Mask = net.CIDRMask(cloakConfig.CidrLenIPv4, 32)
43
+	cloakConfig.ipv6Mask = net.CIDRMask(cloakConfig.CidrLenIPv6, 128)
44
+}
45
+
46
+// simple cloaking algorithm: normalize the IP to its CIDR,
47
+// then hash the resulting bytes with a secret key,
48
+// then truncate to the desired length, b32encode, and append the fake TLD.
49
+func (config *CloakConfig) ComputeCloak(ip net.IP) string {
50
+	if !config.Enabled {
51
+		return ""
52
+	} else if config.NumBits == 0 {
53
+		return config.Netname
54
+	}
55
+	var masked net.IP
56
+	v4ip := ip.To4()
57
+	if v4ip != nil {
58
+		masked = v4ip.Mask(config.ipv4Mask)
59
+	} else {
60
+		masked = ip.Mask(config.ipv6Mask)
61
+	}
62
+	// SHA3(K || M):
63
+	// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
64
+	input := make([]byte, len(config.Secret)+len(masked))
65
+	copy(input, config.Secret[:])
66
+	copy(input[len(config.Secret):], masked)
67
+	digest := sha3.Sum512(input)
68
+	b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
69
+	return fmt.Sprintf("%s.%s", b32digest, config.Netname)
70
+}

+ 9
- 0
irc/config.go View File

@@ -18,6 +18,7 @@ import (
18 18
 	"time"
19 19
 
20 20
 	"code.cloudfoundry.org/bytefmt"
21
+	"github.com/oragono/oragono/irc/cloaks"
21 22
 	"github.com/oragono/oragono/irc/connection_limits"
22 23
 	"github.com/oragono/oragono/irc/custime"
23 24
 	"github.com/oragono/oragono/irc/isupport"
@@ -297,6 +298,7 @@ type Config struct {
297 298
 		isupport            isupport.List
298 299
 		ConnectionLimiter   connection_limits.LimiterConfig   `yaml:"connection-limits"`
299 300
 		ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
301
+		Cloaks              cloaks.CloakConfig                `yaml:"ip-cloaking"`
300 302
 	}
301 303
 
302 304
 	Languages struct {
@@ -728,6 +730,13 @@ func LoadConfig(filename string) (config *Config, err error) {
728 730
 		config.History.ClientLength = 0
729 731
 	}
730 732
 
733
+	config.Server.Cloaks.Initialize()
734
+	if config.Server.Cloaks.Enabled {
735
+		if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
736
+			return nil, fmt.Errorf("You must generate a new value of server.ip-cloaking.secret to enable cloaking")
737
+		}
738
+	}
739
+
731 740
 	for _, listenAddress := range config.Server.TorListeners.Listeners {
732 741
 		found := false
733 742
 		for _, configuredListener := range config.Server.Listen {

+ 2
- 0
irc/gateways.go View File

@@ -70,6 +70,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
70 70
 	ipstring := parsedProxiedIP.String()
71 71
 	client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring)
72 72
 	rawHostname := utils.LookupHostname(ipstring)
73
+	cloakedHostname := client.server.Config().Server.Cloaks.ComputeCloak(parsedProxiedIP)
73 74
 
74 75
 	client.stateMutex.Lock()
75 76
 	defer client.stateMutex.Unlock()
@@ -77,6 +78,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
77 78
 	client.proxiedIP = parsedProxiedIP
78 79
 	session.rawHostname = rawHostname
79 80
 	client.rawHostname = rawHostname
81
+	client.cloakedHostname = cloakedHostname
80 82
 	// nickmask will be updated when the client completes registration
81 83
 	// set tls info
82 84
 	client.certfp = ""

+ 10
- 2
irc/utils/crypto.go View File

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

+ 6
- 1
oragono.go View File

@@ -17,6 +17,7 @@ import (
17 17
 	"github.com/oragono/oragono/irc"
18 18
 	"github.com/oragono/oragono/irc/logger"
19 19
 	"github.com/oragono/oragono/irc/mkcerts"
20
+	"github.com/oragono/oragono/irc/utils"
20 21
 	"golang.org/x/crypto/bcrypt"
21 22
 	"golang.org/x/crypto/ssh/terminal"
22 23
 )
@@ -46,6 +47,7 @@ Usage:
46 47
 	oragono upgradedb [--conf <filename>] [--quiet]
47 48
 	oragono genpasswd [--conf <filename>] [--quiet]
48 49
 	oragono mkcerts [--conf <filename>] [--quiet]
50
+	oragono mksecret [--conf <filename>] [--quiet]
49 51
 	oragono run [--conf <filename>] [--quiet]
50 52
 	oragono -h | --help
51 53
 	oragono --version
@@ -57,7 +59,7 @@ Options:
57 59
 
58 60
 	arguments, _ := docopt.ParseArgs(usage, nil, version)
59 61
 
60
-	// don't require a config file for genpasswd
62
+	// don't require a config file for genpasswd or mksecret
61 63
 	if arguments["genpasswd"].(bool) {
62 64
 		var password string
63 65
 		fd := int(os.Stdin.Fd())
@@ -83,6 +85,9 @@ Options:
83 85
 			fmt.Println()
84 86
 		}
85 87
 		return
88
+	} else if arguments["mksecret"].(bool) {
89
+		fmt.Println(utils.GenerateSecretKey())
90
+		return
86 91
 	}
87 92
 
88 93
 	configfile := arguments["--conf"].(string)

+ 38
- 0
oragono.yaml View File

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

Loading…
Cancel
Save