diff --git a/p2p/dial.go b/p2p/dial.go
index bb3befab24a3c93d1f4eaf646fc012348682b88b..b77971396331a5ffbf2aa96805f20aeb7167ca64 100644
--- a/p2p/dial.go
+++ b/p2p/dial.go
@@ -38,6 +38,10 @@ const (
 	// once every few seconds.
 	lookupInterval = 4 * time.Second
 
+	// If no peers are found for this amount of time, the initial bootnodes are
+	// attempted to be connected.
+	fallbackInterval = 20 * time.Second
+
 	// Endpoint resolution is throttled with bounded backoff.
 	initialResolveDelay = 60 * time.Second
 	maxResolveDelay     = time.Hour
@@ -57,6 +61,9 @@ type dialstate struct {
 	randomNodes   []*discover.Node // filled from Table
 	static        map[discover.NodeID]*dialTask
 	hist          *dialHistory
+
+	start     time.Time        // time when the dialer was first used
+	bootnodes []*discover.Node // default dials when there are no peers
 }
 
 type discoverTable interface {
@@ -102,16 +109,18 @@ type waitExpireTask struct {
 	time.Duration
 }
 
-func newDialState(static []*discover.Node, ntab discoverTable, maxdyn int, netrestrict *netutil.Netlist) *dialstate {
+func newDialState(static []*discover.Node, bootnodes []*discover.Node, ntab discoverTable, maxdyn int, netrestrict *netutil.Netlist) *dialstate {
 	s := &dialstate{
 		maxDynDials: maxdyn,
 		ntab:        ntab,
 		netrestrict: netrestrict,
 		static:      make(map[discover.NodeID]*dialTask),
 		dialing:     make(map[discover.NodeID]connFlag),
+		bootnodes:   make([]*discover.Node, len(bootnodes)),
 		randomNodes: make([]*discover.Node, maxdyn/2),
 		hist:        new(dialHistory),
 	}
+	copy(s.bootnodes, bootnodes)
 	for _, n := range static {
 		s.addStatic(n)
 	}
@@ -130,6 +139,10 @@ func (s *dialstate) removeStatic(n *discover.Node) {
 }
 
 func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now time.Time) []task {
+	if s.start == (time.Time{}) {
+		s.start = now
+	}
+
 	var newtasks []task
 	addDial := func(flag connFlag, n *discover.Node) bool {
 		if err := s.checkDial(n, peers); err != nil {
@@ -169,7 +182,18 @@ func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now
 			newtasks = append(newtasks, t)
 		}
 	}
-
+	// If we don't have any peers whatsoever, try to dial a random bootnode. This
+	// scenario is useful for the testnet (and private networks) where the discovery
+	// table might be full of mostly bad peers, making it hard to find good ones.
+	if len(peers) == 0 && len(s.bootnodes) > 0 && needDynDials > 0 && now.Sub(s.start) > fallbackInterval {
+		bootnode := s.bootnodes[0]
+		s.bootnodes = append(s.bootnodes[:0], s.bootnodes[1:]...)
+		s.bootnodes = append(s.bootnodes, bootnode)
+
+		if addDial(dynDialedConn, bootnode) {
+			needDynDials--
+		}
+	}
 	// Use random nodes from the table for half of the necessary
 	// dynamic dials.
 	randomCandidates := needDynDials / 2
diff --git a/p2p/dial_test.go b/p2p/dial_test.go
index c850233dbac181763fcf22f13432cc6cd412c3c5..08e863bae0a31a2ece5f1f932f1e9a7946fc4360 100644
--- a/p2p/dial_test.go
+++ b/p2p/dial_test.go
@@ -87,7 +87,7 @@ func (t fakeTable) ReadRandomNodes(buf []*discover.Node) int { return copy(buf,
 // This test checks that dynamic dials are launched from discovery results.
 func TestDialStateDynDial(t *testing.T) {
 	runDialTest(t, dialtest{
-		init: newDialState(nil, fakeTable{}, 5, nil),
+		init: newDialState(nil, nil, fakeTable{}, 5, nil),
 		rounds: []round{
 			// A discovery query is launched.
 			{
@@ -219,6 +219,94 @@ func TestDialStateDynDial(t *testing.T) {
 	})
 }
 
+// Tests that bootnodes are dialed if no peers are connectd, but not otherwise.
+func TestDialStateDynDialBootnode(t *testing.T) {
+	bootnodes := []*discover.Node{
+		{ID: uintID(1)},
+		{ID: uintID(2)},
+		{ID: uintID(3)},
+	}
+	table := fakeTable{
+		{ID: uintID(4)},
+		{ID: uintID(5)},
+		{ID: uintID(6)},
+		{ID: uintID(7)},
+		{ID: uintID(8)},
+	}
+	runDialTest(t, dialtest{
+		init: newDialState(nil, bootnodes, table, 5, nil),
+		rounds: []round{
+			// 2 dynamic dials attempted, bootnodes pending fallback interval
+			{
+				new: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}},
+					&discoverTask{},
+				},
+			},
+			// No dials succeed, bootnodes still pending fallback interval
+			{
+				done: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}},
+				},
+			},
+			// No dials succeed, bootnodes still pending fallback interval
+			{},
+			// No dials succeed, 2 dynamic dials attempted and 1 bootnode too as fallback interval was reached
+			{
+				new: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}},
+				},
+			},
+			// No dials succeed, 2nd bootnode is attempted
+			{
+				done: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}},
+				},
+				new: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}},
+				},
+			},
+			// No dials succeed, 3rd bootnode is attempted
+			{
+				done: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}},
+				},
+				new: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}},
+				},
+			},
+			// No dials succeed, 1st bootnode is attempted again, expired random nodes retried
+			{
+				done: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}},
+				},
+				new: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}},
+				},
+			},
+			// Random dial succeeds, no more bootnodes are attempted
+			{
+				peers: []*Peer{
+					{rw: &conn{flags: dynDialedConn, id: uintID(4)}},
+				},
+				done: []task{
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}},
+					&dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}},
+				},
+			},
+		},
+	})
+}
+
 func TestDialStateDynDialFromTable(t *testing.T) {
 	// This table always returns the same random nodes
 	// in the order given below.
@@ -234,7 +322,7 @@ func TestDialStateDynDialFromTable(t *testing.T) {
 	}
 
 	runDialTest(t, dialtest{
-		init: newDialState(nil, table, 10, nil),
+		init: newDialState(nil, nil, table, 10, nil),
 		rounds: []round{
 			// 5 out of 8 of the nodes returned by ReadRandomNodes are dialed.
 			{
@@ -332,7 +420,7 @@ func TestDialStateNetRestrict(t *testing.T) {
 	restrict.Add("127.0.2.0/24")
 
 	runDialTest(t, dialtest{
-		init: newDialState(nil, table, 10, restrict),
+		init: newDialState(nil, nil, table, 10, restrict),
 		rounds: []round{
 			{
 				new: []task{
@@ -355,7 +443,7 @@ func TestDialStateStaticDial(t *testing.T) {
 	}
 
 	runDialTest(t, dialtest{
-		init: newDialState(wantStatic, fakeTable{}, 0, nil),
+		init: newDialState(wantStatic, nil, fakeTable{}, 0, nil),
 		rounds: []round{
 			// Static dials are launched for the nodes that
 			// aren't yet connected.
@@ -436,7 +524,7 @@ func TestDialStateCache(t *testing.T) {
 	}
 
 	runDialTest(t, dialtest{
-		init: newDialState(wantStatic, fakeTable{}, 0, nil),
+		init: newDialState(wantStatic, nil, fakeTable{}, 0, nil),
 		rounds: []round{
 			// Static dials are launched for the nodes that
 			// aren't yet connected.
@@ -498,7 +586,7 @@ func TestDialStateCache(t *testing.T) {
 func TestDialResolve(t *testing.T) {
 	resolved := discover.NewNode(uintID(1), net.IP{127, 0, 55, 234}, 3333, 4444)
 	table := &resolveMock{answer: resolved}
-	state := newDialState(nil, table, 0, nil)
+	state := newDialState(nil, nil, table, 0, nil)
 
 	// Check that the task is generated with an incomplete ID.
 	dest := discover.NewNode(uintID(1), nil, 0, 0)
diff --git a/p2p/server.go b/p2p/server.go
index 48b4e8be3d6502c9c5ff5f0135676528b0b232f6..b2b8c976229493adf4b660b6d887dc098d5f03d3 100644
--- a/p2p/server.go
+++ b/p2p/server.go
@@ -396,7 +396,7 @@ func (srv *Server) Start() (err error) {
 	if !srv.Discovery {
 		dynPeers = 0
 	}
-	dialer := newDialState(srv.StaticNodes, srv.ntab, dynPeers, srv.NetRestrict)
+	dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict)
 
 	// handshake
 	srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)}