← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands-metaserver/ipv6 into lp:widelands-metaserver

 

Notabilis has proposed merging lp:~widelands-dev/widelands-metaserver/ipv6 into lp:widelands-metaserver.

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1689087 in widelands: "Implementing a relay server"
  https://bugs.launchpad.net/widelands/+bug/1689087

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands-metaserver/ipv6/+merge/326028

Updated metaserver to support IPv6.
While doing so, increased maximum supported protocol version from 0 to 1. Connecting with older versions should be fine even after this merge, but they will only be able to connect to IPv4 games.

The related proposed changes in the client are at:
https://code.launchpad.net/~widelands-dev/widelands/net-internetgaming-ipv6
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands-metaserver/ipv6 into lp:widelands-metaserver.
=== modified file 'wlms/client.go'
--- wlms/client.go	2014-02-04 09:45:04 +0000
+++ wlms/client.go	2017-06-20 20:47:51 +0000
@@ -67,6 +67,17 @@
 	// the buildId of Widelands executable that this client is using.
 	buildId string
 
+	// the nonce to link multiple connections by the same client.
+	nonce string
+
+	// the IP of the secondary connection.
+	// usually this is an IPv4 address.
+	secondaryIp string
+
+	// Whether this client has a known IPv4/6 address.
+	hasV4 bool
+	hasV6 bool
+
 	// The game we are currently in. nil if not in game.
 	game *Game
 
@@ -155,6 +166,13 @@
 	client.timeoutTimer.Reset(server.ClientSendingTimeout())
 	client.waitingForPong = false
 
+	ip := net.ParseIP(client.remoteIp())
+	if ip.To4() != nil {
+		client.hasV4 = true
+	} else {
+		client.hasV6 = true
+	}
+
 	for {
 		select {
 		case pkg, ok := <-client.dataStream:
@@ -258,6 +276,10 @@
 	return host
 }
 
+func (client Client) otherIp() string {
+	return client.secondaryIp
+}
+
 func (newClient *Client) successfulRelogin(server *Server, oldClient *Client) {
 	server.RemoveClient(oldClient)
 
@@ -338,10 +360,18 @@
 		return CriticalCmdPacketError{err.Error()}
 	}
 
-	if c.protocolVersion != 0 {
+	if c.protocolVersion != 0 && c.protocolVersion != 1 {
 		return CriticalCmdPacketError{"UNSUPPORTED_PROTOCOL"}
 	}
 
+	if isRegisteredOnServer || c.protocolVersion == 1 {
+		nonce, err := pkg.ReadString()
+		if err != nil {
+			return CriticalCmdPacketError{err.Error()}
+		}
+		c.nonce = nonce
+	}
+
 	if isRegisteredOnServer {
 		if server.HasClient(c.userName) != nil {
 			return CriticalCmdPacketError{"ALREADY_LOGGED_IN"}
@@ -349,11 +379,7 @@
 		if !server.UserDb().ContainsName(c.userName) {
 			return CriticalCmdPacketError{"WRONG_PASSWORD"}
 		}
-		password, err := pkg.ReadString()
-		if err != nil {
-			return CriticalCmdPacketError{err.Error()}
-		}
-		if !server.UserDb().PasswordCorrect(c.userName, password) {
+		if !server.UserDb().PasswordCorrect(c.userName, c.nonce) {
 			return CriticalCmdPacketError{"WRONG_PASSWORD"}
 		}
 		c.permissions = server.UserDb().Permissions(c.userName)
@@ -381,11 +407,19 @@
 func (client *Client) Handle_RELOGIN(server *Server, pkg *packet.Packet) CmdError {
 	var isRegisteredOnServer bool
 	var protocolVersion int
-	var userName, buildId string
+	var userName, buildId, nonce string
 	if err := pkg.Unpack(&protocolVersion, &userName, &buildId, &isRegisteredOnServer); err != nil {
 		return CriticalCmdPacketError{err.Error()}
 	}
 
+	if isRegisteredOnServer || protocolVersion == 1 {
+		n, err := pkg.ReadString()
+		if err != nil {
+			return CriticalCmdPacketError{err.Error()}
+		}
+		nonce = n
+	}
+
 	oldClient := server.HasClient(userName)
 	if oldClient == nil {
 		return CriticalCmdPacketError{"NOT_LOGGED_IN"}
@@ -396,11 +430,7 @@
 			buildId == oldClient.buildId
 
 	if isRegisteredOnServer {
-		password, err := pkg.ReadString()
-		if err != nil {
-			return CriticalCmdPacketError{err.Error()}
-		}
-		if oldClient.permissions == UNREGISTERED || !server.UserDb().PasswordCorrect(userName, password) {
+		if oldClient.permissions == UNREGISTERED || !server.UserDb().PasswordCorrect(userName, nonce) {
 			informationMatches = false
 		}
 	} else if oldClient.permissions != UNREGISTERED {
@@ -417,6 +447,7 @@
 	client.userName = oldClient.userName
 	client.buildId = oldClient.buildId
 	client.game = oldClient.game
+	client.nonce = nonce
 
 	log.Printf("%s wants to reconnect.\n", client.Name())
 	if oldClient.state == RECENTLY_DISCONNECTED {
@@ -432,6 +463,42 @@
 	return nil
 }
 
+func (client *Client) Handle_TELL_IP(server *Server, pkg *packet.Packet) CmdError {
+	var protocolVersion int
+	var name, nonce string
+	if err := pkg.Unpack(&protocolVersion, &name, &nonce); err != nil {
+		return CmdPacketError{err.Error()}
+	}
+
+	if protocolVersion != 1 {
+		return CriticalCmdPacketError{"UNSUPPORTED_PROTOCOL"}
+	}
+
+	old_client := server.HasClient(name)
+	if old_client == nil || old_client.userName != name || old_client.nonce != nonce {
+		log.Printf("Someone failed to register an IP for %s.", old_client.Name())
+		return CriticalCmdPacketError{"NOT_LOGGED_IN"}
+	}
+
+	// We found the existing connection of this client.
+	// Update his IP and close this connection.
+	old_client.secondaryIp = client.remoteIp()
+	ip := net.ParseIP(old_client.otherIp())
+	if ip.To4() != nil {
+		old_client.hasV4 = true
+	} else {
+		old_client.hasV6 = true
+	}
+	log.Printf("%s is now known to use %s and %s.", old_client.Name(), old_client.remoteIp(), old_client.otherIp())
+	client.Disconnect(*server)
+	// Tell the client to get a new list of games. The availability of games might have changed now that
+	// he supports more IP versions
+	old_client.SendPacket("GAMES_UPDATE")
+
+	return nil
+}
+
+
 func (client *Client) Handle_GAME_OPEN(server *Server, pkg *packet.Packet) CmdError {
 	var gameName string
 	var maxPlayer int
@@ -463,9 +530,33 @@
 	}
 
 	host := server.HasClient(game.Host())
-	log.Printf("%s joined %s at IP %s.", client.userName, game.Name(), host.remoteIp())
+	log.Printf("%s joined %s.", client.userName, game.Name())
 
-	client.SendPacket("GAME_CONNECT", host.remoteIp())
+	var ipv4, ipv6 string
+	ip := net.ParseIP(host.remoteIp())
+	if ip.To4() != nil {
+		ipv4 = host.remoteIp()
+		ipv6 = host.otherIp()
+	} else {
+		ipv4 = host.otherIp()
+		ipv6 = host.remoteIp()
+	}
+	if client.protocolVersion == 0 {
+		// Legacy client: Send the IPv4 address
+		client.SendPacket("GAME_CONNECT", ipv4)
+		// One of the two has to be IPv4, otherwise the client wouldn't come this
+		// far anyway (game would appear closed)
+	} else {
+		// Newer client which supports two IPs
+		// Only send him the IPs he can deal with
+		if client.hasV4 && client.hasV6 && host.otherIp() != "" {
+			client.SendPacket("GAME_CONNECT", ipv6, "1", ipv4)
+		} else if client.hasV4 {
+			client.SendPacket("GAME_CONNECT", ipv4, "0")
+		} else if client.hasV6 {
+			client.SendPacket("GAME_CONNECT", ipv6, "0")
+		}
+	}
 	client.setGame(game, server)
 
 	return nil
@@ -526,7 +617,15 @@
 		host := server.HasClient(game.Host())
 		data[n+0] = game.Name()
 		data[n+1] = host.buildId
-		data[n+2] = game.State() == CONNECTABLE
+		// A game is connectable when the client supports the IP version of the game
+		// (and the game is connectable itself, of course)
+		connectable := game.State() == CONNECTABLE_BOTH
+		if client.hasV4 && game.State() == CONNECTABLE_V4 {
+			connectable = true
+		} else if client.hasV6 && game.State() == CONNECTABLE_V6 {
+			connectable = true
+		}
+		data[n+2] = connectable
 		n += 3
 	})
 	client.SendPacket(data...)

=== modified file 'wlms/game.go'
--- wlms/game.go	2014-02-08 20:48:04 +0000
+++ wlms/game.go	2017-06-20 20:47:51 +0000
@@ -3,6 +3,7 @@
 import (
 	"log"
 	"time"
+	"net"
 )
 
 type GameState int
@@ -10,7 +11,9 @@
 const (
 	INITIAL_SETUP GameState = iota
 	NOT_CONNECTABLE
-	CONNECTABLE
+	CONNECTABLE_V4
+	CONNECTABLE_V6
+	CONNECTABLE_BOTH
 	RUNNING
 )
 
@@ -27,11 +30,70 @@
 	C chan bool
 }
 
+func (game *Game) handlePingResult(server *Server, result bool, is_v4 bool) {
+	if result {
+		log.Printf("Successfull ping reply from game %s.", game.Name())
+		switch game.state {
+		case INITIAL_SETUP:
+			if is_v4 {
+				game.SetState(*server, CONNECTABLE_V4)
+			} else {
+				game.SetState(*server, CONNECTABLE_V6)
+			}
+		case NOT_CONNECTABLE:
+			if is_v4 {
+				game.SetState(*server, CONNECTABLE_V4)
+			} else {
+				game.SetState(*server, CONNECTABLE_V6)
+			}
+		case CONNECTABLE_V4:
+			if !is_v4 {
+				game.SetState(*server, CONNECTABLE_BOTH)
+			}
+		case CONNECTABLE_V6:
+			if is_v4 {
+				game.SetState(*server, CONNECTABLE_BOTH)
+			}
+		case CONNECTABLE_BOTH, RUNNING:
+			// Do nothing
+		default:
+			log.Fatalf("Unhandled game.state: %v", game.state)
+		}
+	} else {
+		log.Printf("Failed ping reply from game %s.", game.Name())
+		switch game.state {
+		case INITIAL_SETUP:
+			game.SetState(*server, NOT_CONNECTABLE)
+		case NOT_CONNECTABLE:
+			// Do nothing.
+		case CONNECTABLE_V4:
+			if is_v4 {
+				game.SetState(*server, NOT_CONNECTABLE)
+			}
+		case CONNECTABLE_V6:
+			if !is_v4 {
+				game.SetState(*server, NOT_CONNECTABLE)
+			}
+		case CONNECTABLE_BOTH:
+			if is_v4 {
+				game.SetState(*server, CONNECTABLE_V6)
+			} else {
+				game.SetState(*server, CONNECTABLE_V4)
+			}
+		case RUNNING:
+			// Do nothing
+		default:
+			log.Fatalf("Unhandled game.state: %v", game.state)
+		}
+	}
+}
+
 func (game *Game) pingCycle(server *Server) {
 	// Remember to remove the game when we no longer receive pings.
 	defer server.RemoveGame(game)
 
 	first_ping := true
+	ping_primary_ip := true
 	for {
 		// This game is not even in our list anymore. Give up. If the game has no
 		// host anymore or it has disconnected, remove the game.
@@ -46,37 +108,37 @@
 		if first_ping {
 			pingTimeout = server.GameInitialPingTimeout()
 		}
-		first_ping = false
-
-		pinger := server.NewGamePinger(host, pingTimeout)
-		success, ok := <-pinger.C
-		if success && ok {
-			log.Printf("Successfull ping reply from game %s.", game.Name())
-			switch game.state {
-			case INITIAL_SETUP:
+
+		connected := false
+
+		// The idea is to alternate between pinging the two IP addresses of the client, except for the
+		// first round or if there is only one IP address
+		if ping_primary_ip || host.otherIp() == "" {
+			// Primary IP
+			pinger := server.NewGamePinger(host.remoteIp(), pingTimeout)
+			success, ok := <-pinger.C
+			connected = success && ok
+			game.handlePingResult(server, connected, net.ParseIP(host.remoteIp()).To4() != nil)
+		}
+		if (!ping_primary_ip || first_ping) && host.otherIp() != "" {
+			// Secondary IP
+			pinger := server.NewGamePinger(host.otherIp(), pingTimeout)
+			success, ok := <-pinger.C
+			connected = success && ok
+			game.handlePingResult(server, connected, net.ParseIP(host.otherIp()).To4() != nil)
+
+		}
+		if first_ping {
+			// On first ping, inform the client about the result
+			if connected {
 				host.SendPacket("GAME_OPEN")
-				game.SetState(*server, CONNECTABLE)
-			case NOT_CONNECTABLE:
-				game.SetState(*server, CONNECTABLE)
-			case CONNECTABLE, RUNNING:
-				// Do nothing
-			default:
-				log.Fatalf("Unhandled game.state: %v", game.state)
-			}
-		} else {
-			log.Printf("Failed ping reply from game %s.", game.Name())
-			switch game.state {
-			case INITIAL_SETUP:
+			} else {
 				host.SendPacket("ERROR", "GAME_OPEN", "GAME_TIMEOUT")
-				game.SetState(*server, NOT_CONNECTABLE)
-			case NOT_CONNECTABLE:
-				// Do nothing.
-			case CONNECTABLE, RUNNING:
-				return
-			default:
-				log.Fatalf("Unhandled game.state: %v", game.state)
 			}
+			first_ping = false
 		}
+
+		ping_primary_ip = !ping_primary_ip
 		time.Sleep(server.GamePingTimeout())
 	}
 }

=== modified file 'wlms/main.go'
--- wlms/main.go	2014-01-29 20:01:23 +0000
+++ wlms/main.go	2017-06-20 20:47:51 +0000
@@ -28,6 +28,7 @@
 	var db UserDb
 	var ircbridge IRCBridger
 	if config != "" {
+		log.Println("Loading configuration")
 		var cfg Config
 		if err := cfg.ConfigFrom(config); err != nil {
 			log.Fatalf("Could not parse config file: %v", err)
@@ -39,13 +40,16 @@
 		}
 		ircbridge = NewIRCBridge(cfg.IRCServer, cfg.Realname, cfg.Nickname, cfg.Channel, cfg.UseTLS)
 	} else {
+		log.Println("No configuration found, using in-memory database")
 		db = NewInMemoryDb()
 	}
 	defer db.Close()
 
 	messagesToIrc := make(chan Message, 50)
 	messagesToLobby := make(chan Message, 50)
-	ircbridge.Connect(messagesToIrc, messagesToLobby)
+	if ircbridge != nil {
+		ircbridge.Connect(messagesToIrc, messagesToLobby)
+	}
 	RunServer(db, messagesToLobby, messagesToIrc)
 
 }

=== modified file 'wlms/server.go'
--- wlms/server.go	2014-03-17 18:12:40 +0000
+++ wlms/server.go	2017-06-20 20:47:51 +0000
@@ -38,7 +38,7 @@
 }
 
 type GamePingerFactory interface {
-	New(client *Client, timeout time.Duration) *GamePinger
+	New(ip string, timeout time.Duration) *GamePinger
 }
 
 func (s Server) ClientSendingTimeout() time.Duration {
@@ -96,8 +96,8 @@
 	<-s.serverHasShutdown
 }
 
-func (s *Server) NewGamePinger(client *Client, ping_timeout time.Duration) *GamePinger {
-	return s.gamePingerFactory.New(client, ping_timeout)
+func (s *Server) NewGamePinger(ip string, ping_timeout time.Duration) *GamePinger {
+	return s.gamePingerFactory.New(ip, ping_timeout)
 }
 
 func (s *Server) AddClient(client *Client) {
@@ -241,12 +241,12 @@
 	server *Server
 }
 
-func (gpf RealGamePingerFactory) New(client *Client, timeout time.Duration) *GamePinger {
+func (gpf RealGamePingerFactory) New(ip string, timeout time.Duration) *GamePinger {
 	pinger := &GamePinger{make(chan bool)}
 
 	data := make([]byte, len(NETCMD_METASERVER_PING))
 	go func() {
-		conn, err := net.DialTimeout("tcp", net.JoinHostPort(client.remoteIp(), "7396"), timeout)
+		conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, "7396"), timeout)
 		if err != nil {
 			pinger.C <- false
 			return


Follow ups