3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-26 05:49:25 +01:00

implement ip cloaking

This commit is contained in:
Shivaram Lingamneni 2019-05-12 02:17:57 -04:00
parent 585a6557a4
commit c28e6d13f9
7 changed files with 216 additions and 3 deletions

View File

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

99
irc/cloak_test.go Normal file
View File

@ -0,0 +1,99 @@
// Copyright (c) 2019 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"net"
"testing"
)
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.postprocess()
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.postprocess()
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.postprocess()
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)
}
}

View File

@ -263,6 +263,38 @@ type TorListenersConfig struct {
MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"` MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"`
} }
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) postprocess() {
// 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)
}
// Config defines the overall configuration. // Config defines the overall configuration.
type Config struct { type Config struct {
Network struct { Network struct {
@ -297,6 +329,7 @@ type Config struct {
isupport isupport.List isupport isupport.List
ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"` ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"`
ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"` ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
Cloaks CloakConfig `yaml:"ip-cloaking"`
} }
Languages struct { Languages struct {
@ -728,6 +761,8 @@ func LoadConfig(filename string) (config *Config, err error) {
config.History.ClientLength = 0 config.History.ClientLength = 0
} }
config.Server.Cloaks.postprocess()
for _, listenAddress := range config.Server.TorListeners.Listeners { for _, listenAddress := range config.Server.TorListeners.Listeners {
found := false found := false
for _, configuredListener := range config.Server.Listen { for _, configuredListener := range config.Server.Listen {

View File

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

View File

@ -21,6 +21,8 @@ import (
"time" "time"
"unsafe" "unsafe"
"golang.org/x/crypto/sha3"
"github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/connection_limits" "github.com/oragono/oragono/irc/connection_limits"
@ -283,6 +285,32 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
} }
} }
// 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)
}
// //
// IRC protocol listeners // IRC protocol listeners
// //

View File

@ -11,7 +11,7 @@ import (
var ( var (
// slingamn's own private b32 alphabet, removing 1, l, o, and 0 // 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 ( const (
@ -24,7 +24,7 @@ func GenerateSecretToken() string {
var buf [16]byte var buf [16]byte
rand.Read(buf[:]) rand.Read(buf[:])
// 26 ASCII characters, should be fine for most purposes // 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 // securely check if a supplied token matches a stored token

View File

@ -188,6 +188,45 @@ server:
# - "192.168.1.1" # - "192.168.1.1"
# - "2001:0db8::/32" # - "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 this command:
# python3 -c "import secrets; print(secrets.token_urlsafe())"
# 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 # account options
accounts: accounts:
# account registration # account registration