yggdrasil

package
v0.3.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2021 License: LGPL-3.0 Imports: 37 Imported by: 0

Documentation

Overview

Package yggdrasil implements the core functionality of the Yggdrasil Network.

Introduction

Yggdrasil is a proof-of-concept mesh network which provides end-to-end encrypted communication between nodes in a decentralised fashion. The network is arranged using a globally-agreed spanning tree which provides each node with a locator (coordinates relative to the root) and a distributed hash table (DHT) mechanism for finding other nodes.

Each node also implements a router, which is responsible for encryption of traffic, searches and connections, and a switch, which is responsible ultimately for forwarding traffic across the network.

While many Yggdrasil nodes in existence today are IP nodes - that is, they are transporting IPv6 packets, like a kind of mesh VPN - it is also possible to integrate Yggdrasil into your own applications and use it as a generic data transport, similar to UDP.

This library is what you need to integrate and use Yggdrasil in your own application.

Basics

In order to start an Yggdrasil node, you should start by generating node configuration, which amongst other things, includes encryption keypairs which are used to generate the node's identity, and supply a logger which Yggdrasil's output will be written to.

This may look something like this:

import (
  "os"
  "github.com/gologme/log"
  "github.com/yggdrasil-network/yggdrasil-go/src/config"
  "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
)

type node struct {
  core   yggdrasil.Core
  config *config.NodeConfig
  log    *log.Logger
}

You then can supply node configuration and a logger:

n := node{}
n.log = log.New(os.Stdout, "", log.Flags())
n.config = config.GenerateConfig()

In the above example, we ask the config package to supply new configuration each time, which results in fresh encryption keys and therefore a new identity. It is normally preferable in most cases to persist node configuration onto the filesystem or into some configuration store so that the node's identity does not change each time that the program starts. Note that Yggdrasil will automatically fill in any missing configuration items with sane defaults.

Once you have supplied a logger and some node configuration, you can then start the node:

n.core.Start(n.config, n.log)

Add some peers to connect to the network:

n.core.AddPeer("tcp://some-host.net:54321", "")
n.core.AddPeer("tcp://[2001::1:2:3]:54321", "")
n.core.AddPeer("tcp://1.2.3.4:54321", "")

You can also ask the API for information about our node:

n.log.Println("My node ID is", n.core.NodeID())
n.log.Println("My public key is", n.core.EncryptionPublicKey())
n.log.Println("My coords are", n.core.Coords())

Incoming Connections

Once your node is started, you can then listen for connections from other nodes by asking the API for a Listener:

listener, err := n.core.ConnListen()
if err != nil {
  // ...
}

The Listener has a blocking Accept function which will wait for incoming connections from remote nodes. It will return a Conn when a connection is received. If the node never receives any incoming connections then this function can block forever, so be prepared for that, perhaps by listening in a separate goroutine.

Assuming that you have defined a myConnectionHandler function to deal with incoming connections:

for {
  conn, err := listener.Accept()
  if err != nil {
    // ...
  }

  // We've got a new connection
  go myConnectionHandler(conn)
}

Outgoing Connections

If you know the node ID of the remote node that you want to talk to, you can dial an outbound connection to it. To do this, you should first ask the API for a Dialer:

dialer, err := n.core.ConnDialer()
if err != nil {
  // ...
}

You can then dial using the node's public key in hexadecimal format, for example:

conn, err := dialer.Dial("curve25519", "55071be281f50d0abbda63aadc59755624280c44b2f1f47684317aa4e0325604")
if err != nil {
  // ...
}

Using Connections

Conn objects are implementations of io.ReadWriteCloser, and as such, you can Read, Write and Close them as necessary.

Each Read or Write operation can deal with a buffer with a maximum size of 65535 bytes - any bigger than this and the operation will return an error.

For example, to write to the Conn from the supplied buffer:

buf := []byte{1, 2, 3, 4, 5}
w, err := conn.Write(buf)
if err != nil {
  // ...
} else {
  // written w bytes
}

Reading from the Conn into the supplied buffer:

buf := make([]byte, 65535)
r, err := conn.Read(buf)
if err != nil {
  // ...
} else {
  // read r bytes
}

When you are happy that a connection is no longer required, you can discard it:

err := conn.Close()
if err != nil {
  // ...
}

Limitations

You should be aware of the following limitations when working with the Yggdrasil library:

Individual messages written through Yggdrasil connections can not exceed 65535 bytes in size. Yggdrasil has no concept of fragmentation, so if you try to send a message that exceeds 65535 bytes in size, it will be dropped altogether and an error will be returned.

Yggdrasil connections are unreliable by nature. Messages are delivered on a best-effort basis, and employs congestion control where appropriate to ensure that congestion does not affect message transport, but Yggdrasil will not retransmit any messages that have been lost. If reliable delivery is important then you should manually implement acknowledgement and retransmission of messages.

Index

Constants

View Source
const SwitchQueueTotalMinSize = 4 * 1024 * 1024

Minimum allowed total size of switch queues.

Variables

This section is empty.

Functions

This section is empty.

Types

type Conn

type Conn struct {
	phony.Inbox
	// contains filtered or unexported fields
}

The Conn struct is a reference to an active connection session between the local node and a remote node. Conn implements the io.ReadWriteCloser interface and is used to send and receive traffic with a remote node.

func (*Conn) Close

func (c *Conn) Close() (err error)

Close will close an open connection and any blocking operations on the connection will unblock and return. From this point forward, the connection can no longer be used and you should no longer attempt to Read or Write to the connection.

func (*Conn) LocalAddr

func (c *Conn) LocalAddr() net.Addr

LocalAddr returns the complete public key of the local side of the connection. This is always going to return your own node's public key.

func (*Conn) Read

func (c *Conn) Read(b []byte) (int, error)

Read allows you to read from the connection in a synchronous fashion. The function will block up until the point that either new data is available, the connection has been closed or the read deadline has been reached. If the function succeeds, the number of bytes read from the connection will be returned. Otherwise, an error condition will be returned.

Note that you can also implement asynchronous reads by using SetReadCallback. If you do that, you should no longer attempt to use the Read function.

func (*Conn) RemoteAddr

func (c *Conn) RemoteAddr() net.Addr

RemoteAddr returns the complete public key of the remote side of the connection.

func (*Conn) SetDeadline

func (c *Conn) SetDeadline(t time.Time) error

SetDeadline is equivalent to calling both SetReadDeadline and SetWriteDeadline with the same value, configuring the maximum amount of time that synchronous Read and Write operations can block for. If no deadline is configured, Read and Write operations can potentially block indefinitely.

func (*Conn) SetReadCallback added in v0.3.14

func (c *Conn) SetReadCallback(callback func([]byte))

SetReadCallback allows you to specify a function that will be called whenever a packet is received. This should be used if you wish to implement asynchronous patterns for receiving data from the remote node.

Note that if a read callback has been supplied, you should no longer attempt to use the synchronous Read function.

func (*Conn) SetReadDeadline

func (c *Conn) SetReadDeadline(t time.Time) error

SetReadDeadline configures the maximum amount of time that a synchronous Read operation can block for. A Read operation will unblock at the point that the read deadline is reached if no other condition (such as data arrival or connection closure) happens first. If no deadline is configured, Read operations can potentially block indefinitely.

func (*Conn) SetWriteDeadline

func (c *Conn) SetWriteDeadline(t time.Time) error

SetWriteDeadline configures the maximum amount of time that a synchronous Write operation can block for. A Write operation will unblock at the point that the read deadline is reached if no other condition (such as data sending or connection closure) happens first. If no deadline is configured, Write operations can potentially block indefinitely.

func (*Conn) String

func (c *Conn) String() string

String returns a string that uniquely identifies a connection. Currently this takes a form similar to "conn=0x0000000", which contains a memory reference to the Conn object. While this value should always be unique for each Conn object, the format of this is not strictly defined and may change in the future.

func (*Conn) Write

func (c *Conn) Write(b []byte) (int, error)

Write allows you to write to the connection in a synchronous fashion. This function may block until either the write has completed, the connection has been closed or the write deadline has been reached. If the function succeeds, the number of written bytes is returned. Otherwise, an error condition is returned.

func (*Conn) WriteFrom added in v0.3.14

func (c *Conn) WriteFrom(from phony.Actor, msg FlowKeyMessage, callback func(error))

WriteFrom should be called by a phony.Actor, and tells the Conn to send a message. This is used internally by Write. If the callback is called with a non-nil value, then it is safe to reuse the argument FlowKeyMessage.

type ConnError

type ConnError struct {
	// contains filtered or unexported fields
}

ConnError implements the net.Error interface

func (*ConnError) Closed

func (e *ConnError) Closed() bool

Closed returns if the session is already closed and is now unusable.

func (*ConnError) PacketMaximumSize

func (e *ConnError) PacketMaximumSize() int

PacketMaximumSize returns the maximum supported packet size. This will only return a non-zero value if ConnError.PacketTooBig() returns true.

func (*ConnError) PacketTooBig

func (e *ConnError) PacketTooBig() bool

PacketTooBig returns in response to sending a packet that is too large, and if so, the maximum supported packet size that should be used for the connection.

func (*ConnError) Temporary

func (e *ConnError) Temporary() bool

Temporary return true if the error is temporary or false if it is a permanent error condition.

func (*ConnError) Timeout

func (e *ConnError) Timeout() bool

Timeout returns true if the error relates to a timeout condition on the connection.

type Core

type Core struct {
	// This is the main data structure that holds everything else for a node
	// We're going to keep our own copy of the provided config - that way we can
	// guarantee that it will be covered by the mutex
	phony.Inbox
	// contains filtered or unexported fields
}

The Core object represents the Yggdrasil node. You should create a Core object for each Yggdrasil node you plan to run.

func (*Core) AddAllowedEncryptionPublicKey

func (c *Core) AddAllowedEncryptionPublicKey(bstr string) (err error)

AddAllowedEncryptionPublicKey whitelists a key for incoming peer connections. By default all incoming peer connections are accepted, but adding public keys to the whitelist using this function enables strict checking from that point forward. Once the whitelist is enabled, only peer connections from whitelisted public keys will be accepted.

func (*Core) AddPeer

func (c *Core) AddPeer(addr string, sintf string) error

AddPeer adds a peer. This should be specified in the peer URI format, e.g.:

tcp://a.b.c.d:e
socks://a.b.c.d:e/f.g.h.i:j

This adds the peer to the peer list, so that they will be called again if the connection drops.

func (*Core) Address

func (c *Core) Address() net.IP

Address gets the IPv6 address of the Yggdrasil node. This is always a /128 address. The IPv6 address is only relevant when the node is operating as an IP router and often is meaningless when embedded into an application, unless that application also implements either VPN functionality or deals with IP packets specifically.

func (*Core) CallPeer

func (c *Core) CallPeer(addr string, sintf string) error

CallPeer calls a peer once. This should be specified in the peer URI format, e.g.:

tcp://a.b.c.d:e
socks://a.b.c.d:e/f.g.h.i:j

This does not add the peer to the peer list, so if the connection drops, the peer will not be called again automatically.

func (*Core) ConnDialer

func (c *Core) ConnDialer() (*Dialer, error)

ConnDialer returns a dialer for Yggdrasil session connections. Since ConnDialers are stateless, you can request as many dialers as you like, although ideally you should request only one and keep the reference to it for as long as it is needed.

func (*Core) ConnListen

func (c *Core) ConnListen() (*Listener, error)

ConnListen returns a listener for Yggdrasil session connections. You can only call this function once as each Yggdrasil node can only have a single ConnListener. Make sure to keep the reference to this for as long as it is needed.

func (*Core) Coords

func (c *Core) Coords() []uint64

Coords returns the current coordinates of the node. Note that these can change at any time for a number of reasons, not limited to but including changes to peerings (either yours or a parent nodes) or changes to the network root.

This function may return an empty array - this is normal behaviour if either you are the root of the network that you are connected to, or you are not connected to any other nodes (effectively making you the root of a single-node network).

func (*Core) DHTPing

func (c *Core) DHTPing(key crypto.BoxPubKey, coords []uint64, target *crypto.NodeID) (DHTRes, error)

DHTPing sends a DHT ping to the node with the provided key and coords, optionally looking up the specified target NodeID.

func (*Core) DisconnectPeer

func (c *Core) DisconnectPeer(port uint64) error

DisconnectPeer disconnects a peer once. This should be specified as a port number.

func (*Core) EncryptionPublicKey

func (c *Core) EncryptionPublicKey() string

EncryptionPublicKey gets the node's encryption public key, as used by the router.

func (*Core) GetAllowedEncryptionPublicKeys

func (c *Core) GetAllowedEncryptionPublicKeys() []string

GetAllowedEncryptionPublicKeys returns the public keys permitted for incoming peer connections. If this list is empty then all incoming peer connections are accepted by default.

func (*Core) GetDHT

func (c *Core) GetDHT() []DHTEntry

GetDHT returns zero or more entries as stored in the DHT, cached primarily from searches that have already taken place.

func (*Core) GetMaximumSessionMTU added in v0.3.14

func (c *Core) GetMaximumSessionMTU() MTU

GetMaximumSessionMTU returns the maximum allowed session MTU size.

func (*Core) GetNodeInfo

func (c *Core) GetNodeInfo(key crypto.BoxPubKey, coords []uint64, nocache bool) (NodeInfoPayload, error)

GetNodeInfo requests nodeinfo from a remote node, as specified by the public key and coordinates specified. The third parameter specifies whether a cached result is acceptable - this results in less traffic being generated than is necessary when, e.g. crawling the network.

func (*Core) GetPeers

func (c *Core) GetPeers() []Peer

GetPeers returns one or more Peer objects containing information about active peerings with other Yggdrasil nodes, where one of the responses always includes information about the current node (with a port number of 0). If there is exactly one entry then this node is not connected to any other nodes and is therefore isolated.

func (*Core) GetSessions

func (c *Core) GetSessions() []Session

GetSessions returns a list of open sessions from this node to other nodes.

func (*Core) GetSwitchPeers

func (c *Core) GetSwitchPeers() []SwitchPeer

GetSwitchPeers returns zero or more SwitchPeer objects containing information about switch port connections with other Yggdrasil nodes. Note that, unlike GetPeers, GetSwitchPeers does not include information about the current node, therefore it is possible for this to return zero elements if the node is isolated or not connected to any peers.

func (*Core) ListenTCP

func (c *Core) ListenTCP(uri string) (*TcpListener, error)

ListenTCP starts a new TCP listener. The input URI should match that of the "Listen" configuration item, e.g.

tcp://a.b.c.d:e

func (*Core) ListenTLS added in v0.3.14

func (c *Core) ListenTLS(uri string) (*TcpListener, error)

ListenTLS starts a new TLS listener. The input URI should match that of the "Listen" configuration item, e.g.

tls://a.b.c.d:e

func (*Core) MyNodeInfo

func (c *Core) MyNodeInfo() NodeInfoPayload

MyNodeInfo gets the currently configured nodeinfo. NodeInfo is typically specified through the "NodeInfo" option in the node configuration or using the SetNodeInfo function, although it may also contain other built-in values such as "buildname", "buildversion" etc.

func (c *Core) NewSimlink() *Simlink

func (*Core) NodeID

func (c *Core) NodeID() *crypto.NodeID

NodeID gets the node ID. This is derived from your router encryption keys. Remote nodes wanting to open connections to your node will need to know your node ID.

func (*Core) RemoveAllowedEncryptionPublicKey

func (c *Core) RemoveAllowedEncryptionPublicKey(bstr string) (err error)

RemoveAllowedEncryptionPublicKey removes a key from the whitelist for incoming peer connections. If none are set, an empty list permits all incoming connections.

func (*Core) RemovePeer

func (c *Core) RemovePeer(addr string, sintf string) error

func (*Core) SetLogger

func (c *Core) SetLogger(log *log.Logger)

SetLogger sets the output logger of the Yggdrasil node after startup. This may be useful if you want to redirect the output later. Note that this expects a Logger from the github.com/gologme/log package and not from Go's built-in log package.

func (*Core) SetMaximumSessionMTU added in v0.3.14

func (c *Core) SetMaximumSessionMTU(mtu MTU)

SetMaximumSessionMTU sets the maximum allowed session MTU size. The default value is 65535 bytes. Session pings will be sent to update all open sessions if the MTU has changed.

func (*Core) SetNodeInfo

func (c *Core) SetNodeInfo(nodeinfo interface{}, nodeinfoprivacy bool)

SetNodeInfo sets the local nodeinfo. Note that nodeinfo can be any value or struct, it will be serialised into JSON automatically.

func (*Core) SetSessionGatekeeper

func (c *Core) SetSessionGatekeeper(f func(pubkey *crypto.BoxPubKey, initiator bool) bool)

SetSessionGatekeeper allows you to configure a handler function for deciding whether a session should be allowed or not. The default session firewall is implemented in this way. The function receives the public key of the remote side and a boolean which is true if we initiated the session or false if we received an incoming session request. The function should return true to allow the session or false to reject it.

func (*Core) SigningPublicKey

func (c *Core) SigningPublicKey() string

SigningPublicKey gets the node's signing public key, as used by the switch.

func (*Core) Start

func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) (conf *config.NodeState, err error)

Start starts up Yggdrasil using the provided config.NodeConfig, and outputs debug logging through the provided log.Logger. The started stack will include TCP and UDP sockets, a multicast discovery socket, an admin socket, router, switch and DHT node. A config.NodeState is returned which contains both the current and previous configurations (from reconfigures).

func (*Core) Stop

func (c *Core) Stop()

Stop shuts down the Yggdrasil node.

func (*Core) Subnet

func (c *Core) Subnet() net.IPNet

Subnet gets the routed IPv6 subnet of the Yggdrasil node. This is always a /64 subnet. The IPv6 subnet is only relevant when the node is operating as an IP router and often is meaningless when embedded into an application, unless that application also implements either VPN functionality or deals with IP packets specifically.

func (*Core) TreeID

func (c *Core) TreeID() *crypto.TreeID

TreeID gets the tree ID. This is derived from your switch signing keys. There is typically no need to share this key.

func (*Core) UpdateConfig

func (c *Core) UpdateConfig(config *config.NodeConfig)

UpdateConfig updates the configuration in Core with the provided config.NodeConfig and then signals the various module goroutines to reconfigure themselves if needed.

type DHTEntry

type DHTEntry struct {
	PublicKey crypto.BoxPubKey
	Coords    []uint64
	LastSeen  time.Duration
}

DHTEntry represents a single DHT entry that has been learned or cached from DHT searches.

type DHTRes

type DHTRes struct {
	PublicKey crypto.BoxPubKey // key of the sender
	Coords    []uint64         // coords of the sender
	Dest      crypto.NodeID    // the destination node ID
	Infos     []DHTEntry       // response
}

DHTRes represents a DHT response, as returned by DHTPing.

type Dialer

type Dialer struct {
	// contains filtered or unexported fields
}

Dialer represents an Yggdrasil connection dialer.

func (*Dialer) Dial

func (d *Dialer) Dial(network, address string) (net.Conn, error)

Dial opens a session to the given node. The first parameter should be "curve25519" or "nodeid" and the second parameter should contain a hexadecimal representation of the target. It uses DialContext internally.

func (*Dialer) DialByNodeIDandMask

func (d *Dialer) DialByNodeIDandMask(ctx context.Context, nodeID, nodeMask *crypto.NodeID) (net.Conn, error)

DialByNodeIDandMask opens a session to the given node based on raw NodeID parameters. If ctx is nil or has no timeout, then a default timeout of 6 seconds will apply, beginning *after* the search finishes.

func (*Dialer) DialByPublicKey added in v0.3.14

func (d *Dialer) DialByPublicKey(ctx context.Context, pubKey *crypto.BoxPubKey) (net.Conn, error)

DialByPublicKey opens a session to the given node based on the public key. If ctx is nil or has no timeout, then a default timeout of 6 seconds will apply, beginning *after* the search finishes.

func (*Dialer) DialContext added in v0.3.14

func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error)

DialContext is used internally by Dial, and should only be used with a context that includes a timeout. It uses DialByNodeIDandMask internally when the network is "nodeid", or DialByPublicKey when the network is "curve25519".

type FlowKeyMessage

type FlowKeyMessage struct {
	FlowKey uint64
	Message []byte
}

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener waits for incoming sessions

func (*Listener) Accept

func (l *Listener) Accept() (net.Conn, error)

Accept blocks until a new incoming session is received

func (*Listener) Addr

func (l *Listener) Addr() net.Addr

Addr returns the address of the listener

func (*Listener) Close

func (l *Listener) Close() (err error)

Close will stop the listener

type MTU added in v0.3.14

type MTU = types.MTU

type NodeInfoPayload

type NodeInfoPayload []byte

NodeInfoPayload represents a RequestNodeInfo response, in bytes.

type Peer

type Peer struct {
	PublicKey  crypto.BoxPubKey // The public key of the remote node
	Endpoint   string           // The connection string used to connect to the peer
	BytesSent  uint64           // Number of bytes sent to this peer
	BytesRecvd uint64           // Number of bytes received from this peer
	Protocol   string           // The transport protocol that this peer is connected with, typically "tcp"
	Port       uint64           // Switch port number for this peer connection
	Uptime     time.Duration    // How long this peering has been active for
}

Peer represents a single peer object. This contains information from the preferred switch port for this peer, although there may be more than one active switch port connection to the peer in reality.

This struct is informational only - you cannot manipulate peer connections using instances of this struct. You should use the AddPeer or RemovePeer functions instead.

type Session

type Session struct {
	PublicKey   crypto.BoxPubKey // The public key of the remote node
	Coords      []uint64         // The coordinates of the remote node
	BytesSent   uint64           // Bytes sent to the session
	BytesRecvd  uint64           // Bytes received from the session
	MTU         MTU              // The maximum supported message size of the session
	Uptime      time.Duration    // How long this session has been active for
	WasMTUFixed bool             // This field is no longer used
}

Session represents an open session with another node. Sessions are opened in response to traffic being exchanged between two nodes using Conn objects. Note that sessions will automatically be closed by Yggdrasil if no traffic is exchanged for around two minutes.

type Simlink struct {
	phony.Inbox
	// contains filtered or unexported fields
}

func (*Simlink) SetDestination added in v0.3.16

func (s *Simlink) SetDestination(dest *Simlink) error

func (*Simlink) Start added in v0.3.16

func (s *Simlink) Start() error

type SwitchPeer

type SwitchPeer struct {
	PublicKey  crypto.BoxPubKey // The public key of the remote node
	Coords     []uint64         // The coordinates of the remote node
	BytesSent  uint64           // Number of bytes sent via this switch port
	BytesRecvd uint64           // Number of bytes received via this switch port
	Port       uint64           // Switch port number for this switch peer
	Protocol   string           // The transport protocol that this switch port is connected with, typically "tcp"
	Endpoint   string           // The connection string used to connect to the switch peer
}

SwitchPeer represents a switch connection to a peer. Note that there may be multiple switch peers per actual peer, e.g. if there are multiple connections to a given node.

This struct is informational only - you cannot manipulate switch peer connections using instances of this struct. You should use the AddPeer or RemovePeer functions instead.

type SwitchQueue

type SwitchQueue struct {
	ID      string // The ID of the switch queue
	Size    uint64 // The total size, in bytes, of the queue
	Packets uint64 // The number of packets in the queue
	Port    uint64 // The switch port to which the queue applies
}

SwitchQueue represents a single switch queue. Switch queues are only created in response to congestion on a given link and represent how much data has been temporarily cached for sending once the congestion has cleared.

type SwitchQueues

type SwitchQueues struct {
	Queues       []SwitchQueue // An array of SwitchQueue objects containing information about individual queues
	Count        uint64        // The current number of active switch queues
	Size         uint64        // The current total size of active switch queues
	HighestCount uint64        // The highest recorded number of switch queues so far
	HighestSize  uint64        // The highest recorded total size of switch queues so far
	MaximumSize  uint64        // The maximum allowed total size of switch queues, as specified by config
}

SwitchQueues represents information from the switch related to link congestion and a list of switch queues created in response to congestion on a given link.

type TcpListener

type TcpListener struct {
	Listener net.Listener
	// contains filtered or unexported fields
}

TcpListener is a stoppable TCP listener interface. These are typically returned from calls to the ListenTCP() function and are also used internally to represent listeners created by the "Listen" configuration option and for multicast interfaces.

func (*TcpListener) Stop

func (l *TcpListener) Stop()

type TcpUpgrade added in v0.3.14

type TcpUpgrade struct {
	// contains filtered or unexported fields
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL