Browse Source

implement ip cloaking

tags/v1.1.0-rc1
Shivaram Lingamneni 5 years ago
parent
commit
c28e6d13f9
7 changed files with 216 additions and 3 deletions
  1. 11
    1
      irc/client.go
  2. 99
    0
      irc/cloak_test.go
  3. 35
    0
      irc/config.go
  4. 2
    0
      irc/gateways.go
  5. 28
    0
      irc/server.go
  6. 2
    2
      irc/utils/crypto.go
  7. 39
    0
      oragono.yaml

+ 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)

+ 99
- 0
irc/cloak_test.go View File

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

+ 35
- 0
irc/config.go View File

@@ -263,6 +263,38 @@ type TorListenersConfig struct {
263 263
 	MaxConnectionsPerDuration int           `yaml:"max-connections-per-duration"`
264 264
 }
265 265
 
266
+type CloakConfig struct {
267
+	Enabled     bool
268
+	Netname     string
269
+	Secret      string
270
+	CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
271
+	CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
272
+	NumBits     int `yaml:"num-bits"`
273
+
274
+	numBytes int
275
+	ipv4Mask net.IPMask
276
+	ipv6Mask net.IPMask
277
+}
278
+
279
+func (cloakConfig *CloakConfig) postprocess() {
280
+	// sanity checks:
281
+	numBits := cloakConfig.NumBits
282
+	if 0 == numBits {
283
+		numBits = 80
284
+	} else if 256 < numBits {
285
+		numBits = 256
286
+	}
287
+
288
+	// derived values:
289
+	cloakConfig.numBytes = numBits / 8
290
+	// round up to the nearest byte
291
+	if numBits%8 != 0 {
292
+		cloakConfig.numBytes += 1
293
+	}
294
+	cloakConfig.ipv4Mask = net.CIDRMask(cloakConfig.CidrLenIPv4, 32)
295
+	cloakConfig.ipv6Mask = net.CIDRMask(cloakConfig.CidrLenIPv6, 128)
296
+}
297
+
266 298
 // Config defines the overall configuration.
267 299
 type Config struct {
268 300
 	Network struct {
@@ -297,6 +329,7 @@ type Config struct {
297 329
 		isupport            isupport.List
298 330
 		ConnectionLimiter   connection_limits.LimiterConfig   `yaml:"connection-limits"`
299 331
 		ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
332
+		Cloaks              CloakConfig                       `yaml:"ip-cloaking"`
300 333
 	}
301 334
 
302 335
 	Languages struct {
@@ -728,6 +761,8 @@ func LoadConfig(filename string) (config *Config, err error) {
728 761
 		config.History.ClientLength = 0
729 762
 	}
730 763
 
764
+	config.Server.Cloaks.postprocess()
765
+
731 766
 	for _, listenAddress := range config.Server.TorListeners.Listeners {
732 767
 		found := false
733 768
 		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 = ""

+ 28
- 0
irc/server.go View File

@@ -21,6 +21,8 @@ import (
21 21
 	"time"
22 22
 	"unsafe"
23 23
 
24
+	"golang.org/x/crypto/sha3"
25
+
24 26
 	"github.com/goshuirc/irc-go/ircfmt"
25 27
 	"github.com/oragono/oragono/irc/caps"
26 28
 	"github.com/oragono/oragono/irc/connection_limits"
@@ -283,6 +285,32 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
283 285
 	}
284 286
 }
285 287
 
288
+// simple cloaking algorithm: normalize the IP to its CIDR,
289
+// then hash the resulting bytes with a secret key,
290
+// then truncate to the desired length, b32encode, and append the fake TLD.
291
+func (config *CloakConfig) ComputeCloak(ip net.IP) string {
292
+	if !config.Enabled {
293
+		return ""
294
+	} else if config.NumBits == 0 {
295
+		return config.Netname
296
+	}
297
+	var masked net.IP
298
+	v4ip := ip.To4()
299
+	if v4ip != nil {
300
+		masked = v4ip.Mask(config.ipv4Mask)
301
+	} else {
302
+		masked = ip.Mask(config.ipv6Mask)
303
+	}
304
+	// SHA3(K || M):
305
+	// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
306
+	input := make([]byte, len(config.Secret)+len(masked))
307
+	copy(input, config.Secret[:])
308
+	copy(input[len(config.Secret):], masked)
309
+	digest := sha3.Sum512(input)
310
+	b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
311
+	return fmt.Sprintf("%s.%s", b32digest, config.Netname)
312
+}
313
+
286 314
 //
287 315
 // IRC protocol listeners
288 316
 //

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

@@ -11,7 +11,7 @@ import (
11 11
 
12 12
 var (
13 13
 	// slingamn's own private b32 alphabet, removing 1, l, o, and 0
14
-	b32encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
14
+	B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
15 15
 )
16 16
 
17 17
 const (
@@ -24,7 +24,7 @@ func GenerateSecretToken() string {
24 24
 	var buf [16]byte
25 25
 	rand.Read(buf[:])
26 26
 	// 26 ASCII characters, should be fine for most purposes
27
-	return b32encoder.EncodeToString(buf[:])
27
+	return B32Encoder.EncodeToString(buf[:])
28 28
 }
29 29
 
30 30
 // securely check if a supplied token matches a stored token

+ 39
- 0
oragono.yaml View File

@@ -188,6 +188,45 @@ 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 this command:
208
+        # python3 -c "import secrets; print(secrets.token_urlsafe())"
209
+        # note that rotating this key will invalidate all existing ban masks.
210
+        secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4"
211
+
212
+        # the cloaked hostname is derived only from the CIDR (most significant bits
213
+        # of the IP address), up to a configurable number of bits. this is the
214
+        # granularity at which bans will take effect for ipv4 (a /32 is a fully
215
+        # specified IP address). note that changing this value will invalidate
216
+        # any stored bans.
217
+        cidr-len-ipv4: 32
218
+
219
+        # analogous value for ipv6 (an ipv6 /64 is the typical prefix assigned
220
+        # by an ISP to an individual customer for their LAN)
221
+        cidr-len-ipv6: 64
222
+
223
+        # number of bits of hash output to include in the cloaked hostname.
224
+        # more bits means less likelihood of distinct IPs colliding,
225
+        # at the cost of a longer cloaked hostname. if this value is set to 0,
226
+        # all users will receive simply `netname` as their cloaked hostname.
227
+        num-bits: 80
228
+
229
+
191 230
 # account options
192 231
 accounts:
193 232
     # account registration

Loading…
Cancel
Save