diff --git a/conventional.yaml b/conventional.yaml index 46d47844..ace5ad36 100644 --- a/conventional.yaml +++ b/conventional.yaml @@ -269,6 +269,12 @@ server: # whether to enable IP cloaking enabled: false + # whether to use these cloak settings (specifically, `netname` and `num-bits`) + # to produce unique hostnames for always-on clients. you can enable this even if + # you disabled IP cloaking for normal clients above. if this is disabled, + # always-on clients will all have an identical hostname (the server name). + enabled-for-always-on: true + # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc # you may want to use your network name here netname: "irc" diff --git a/default.yaml b/default.yaml index 25140484..6af01d90 100644 --- a/default.yaml +++ b/default.yaml @@ -297,6 +297,12 @@ server: # whether to enable IP cloaking enabled: true + # whether to use these cloak settings (specifically, `netname` and `num-bits`) + # to produce unique hostnames for always-on clients. you can enable this even if + # you disabled IP cloaking for normal clients above. if this is disabled, + # always-on clients will all have an identical hostname (the server name). + enabled-for-always-on: true + # fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.irc # you may want to use your network name here netname: "irc" diff --git a/irc/client.go b/irc/client.go index 788c4ae2..c03831ba 100644 --- a/irc/client.go +++ b/irc/client.go @@ -411,6 +411,11 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen = map[string]time.Time{"": now} } + hostname := server.name + if config.Server.Cloaks.EnabledForAlwaysOn { + hostname = config.Server.Cloaks.ComputeAccountCloak(account.Name) + } + client := &Client{ lastSeen: lastSeen, lastActive: now, @@ -419,9 +424,8 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, languages: server.Languages().Default(), server: server, - // TODO figure out how to set these on reattach? username: "~user", - rawHostname: server.name, + rawHostname: hostname, realIP: utils.IPv4LoopbackAddress, alwaysOn: true, diff --git a/irc/cloaks/cloak_test.go b/irc/cloaks/cloak_test.go index 617c427a..2b67249c 100644 --- a/irc/cloaks/cloak_test.go +++ b/irc/cloaks/cloak_test.go @@ -104,3 +104,25 @@ func BenchmarkCloaks(b *testing.B) { config.ComputeCloak(v6ip) } } + +func TestAccountCloak(t *testing.T) { + config := cloakConfForTesting() + + // just assert that we get all distinct values + assertEqual(config.ComputeAccountCloak("shivaram"), "8yu8kunudb45ztxm.oragono", t) + assertEqual(config.ComputeAccountCloak("dolph🐬n"), "hhgeqsvzeagv3wjw.oragono", t) + assertEqual(config.ComputeAccountCloak("SHIVARAM"), "bgx32x4r7qzih4uh.oragono", t) + assertEqual(config.ComputeAccountCloak("ed"), "j5autmgxtdjdyzf4.oragono", t) +} + +func TestAccountCloakCollisions(t *testing.T) { + config := cloakConfForTesting() + + v4ip := easyParseIP("97.97.97.97") + v4cloak := config.ComputeCloak(v4ip) + // "aaaa" is the same bytestring as 97.97.97.97 + aaaacloak := config.ComputeAccountCloak("aaaa") + if v4cloak == aaaacloak { + t.Errorf("cloak collision between 97.97.97.97 and aaaa: %s", v4cloak) + } +} diff --git a/irc/cloaks/cloaks.go b/irc/cloaks/cloaks.go index 975022ea..0418d993 100644 --- a/irc/cloaks/cloaks.go +++ b/irc/cloaks/cloaks.go @@ -12,12 +12,13 @@ import ( ) type CloakConfig struct { - Enabled bool - Netname string - CidrLenIPv4 int `yaml:"cidr-len-ipv4"` - CidrLenIPv6 int `yaml:"cidr-len-ipv6"` - NumBits int `yaml:"num-bits"` - LegacySecretValue string `yaml:"secret"` + Enabled bool + EnabledForAlwaysOn bool `yaml:"enabled-for-always-on"` + Netname string + CidrLenIPv4 int `yaml:"cidr-len-ipv4"` + CidrLenIPv6 int `yaml:"cidr-len-ipv6"` + NumBits int `yaml:"num-bits"` + LegacySecretValue string `yaml:"secret"` secret string numBytes int @@ -26,14 +27,10 @@ type CloakConfig struct { } func (cloakConfig *CloakConfig) Initialize() { - if !cloakConfig.Enabled { - return - } - // sanity checks: numBits := cloakConfig.NumBits if 0 == numBits { - numBits = 80 + numBits = 64 } else if 256 < numBits { numBits = 256 } @@ -69,12 +66,30 @@ func (config *CloakConfig) ComputeCloak(ip net.IP) string { } else { masked = ip.Mask(config.ipv6Mask) } + return config.macAndCompose(masked) +} + +func (config *CloakConfig) macAndCompose(b []byte) string { // 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)) + input := make([]byte, len(config.secret)+len(b)) copy(input, config.secret[:]) - copy(input[len(config.secret):], masked) + copy(input[len(config.secret):], b) digest := sha3.Sum512(input) b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes]) return fmt.Sprintf("%s.%s", b32digest, config.Netname) } + +func (config *CloakConfig) ComputeAccountCloak(accountName string) string { + // XXX don't bother checking EnabledForAlwaysOn, since if it's disabled, + // we need to use the server name which we don't have + if config.NumBits == 0 || config.secret == "" { + return config.Netname + } + + // pad with 16 initial bytes of zeroes, avoiding any possibility of collision + // with a masked IP that could be an input to ComputeCloak: + paddedAccountName := make([]byte, 16+len(accountName)) + copy(paddedAccountName[16:], accountName[:]) + return config.macAndCompose(paddedAccountName) +} diff --git a/irc/server.go b/irc/server.go index bd83423e..5b0f5593 100644 --- a/irc/server.go +++ b/irc/server.go @@ -599,9 +599,7 @@ func (server *Server) applyConfig(config *Config) (err error) { // now that the datastore is initialized, we can load the cloak secret from it // XXX this modifies config after the initial load, which is naughty, // but there's no data race because we haven't done SetConfig yet - if config.Server.Cloaks.Enabled { - config.Server.Cloaks.SetSecret(LoadCloakSecret(server.store)) - } + config.Server.Cloaks.SetSecret(LoadCloakSecret(server.store)) // activate the new config server.SetConfig(config)