2018-01-21 00:17:15 +00:00
package yggdrasil
2018-06-12 22:50:08 +00:00
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"os"
"sort"
"strconv"
"strings"
"sync/atomic"
"time"
2018-07-07 11:08:52 +00:00
"yggdrasil/defaults"
2018-06-12 22:50:08 +00:00
)
2018-01-21 00:17:15 +00:00
2018-01-21 12:57:54 +00:00
// TODO: Add authentication
2018-01-21 00:17:15 +00:00
type admin struct {
core * Core
listenaddr string
2018-07-07 19:04:11 +00:00
listener net . Listener
2018-01-30 00:48:14 +00:00
handlers [ ] admin_handlerInfo
}
2018-05-20 16:21:14 +00:00
type admin_info map [ string ] interface { }
2018-01-30 00:48:14 +00:00
type admin_handlerInfo struct {
2018-05-20 16:21:14 +00:00
name string // Checked against the first word of the api call
args [ ] string // List of human-readable argument names
handler func ( admin_info ) ( admin_info , error ) // First is input map, second is output
}
2018-06-10 23:03:28 +00:00
// admin_pair maps things like "IP", "port", "bucket", or "coords" onto values.
2018-05-20 16:21:14 +00:00
type admin_pair struct {
key string
val interface { }
2018-01-30 00:48:14 +00:00
}
2018-06-10 23:03:28 +00:00
// admin_nodeInfo represents the information we know about a node for an admin response.
2018-05-20 16:21:14 +00:00
type admin_nodeInfo [ ] admin_pair
2018-01-30 00:48:14 +00:00
2018-06-10 23:03:28 +00:00
// addHandler is called for each admin function to add the handler and help documentation to the API.
2018-05-20 16:21:14 +00:00
func ( a * admin ) addHandler ( name string , args [ ] string , handler func ( admin_info ) ( admin_info , error ) ) {
2018-01-30 00:48:14 +00:00
a . handlers = append ( a . handlers , admin_handlerInfo { name , args , handler } )
2018-01-21 00:17:15 +00:00
}
2018-06-10 23:03:28 +00:00
// init runs the initial admin setup.
2018-01-21 00:17:15 +00:00
func ( a * admin ) init ( c * Core , listenaddr string ) {
a . core = c
a . listenaddr = listenaddr
2018-11-27 01:15:27 +00:00
a . addHandler ( "list" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-21 12:54:51 +00:00
handlers := make ( map [ string ] interface { } )
2018-01-30 00:48:14 +00:00
for _ , handler := range a . handlers {
2018-05-21 13:14:57 +00:00
handlers [ handler . name ] = admin_info { "fields" : handler . args }
2018-01-30 00:48:14 +00:00
}
2018-11-26 17:51:30 +00:00
return admin_info { "list" : handlers } , nil
2018-01-30 00:48:14 +00:00
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "dot" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-20 16:21:14 +00:00
return admin_info { "dot" : string ( a . getResponse_dot ( ) ) } , nil
2018-01-30 00:48:14 +00:00
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "getSelf" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-21 12:54:51 +00:00
self := a . getData_getSelf ( ) . asMap ( )
ip := fmt . Sprint ( self [ "ip" ] )
delete ( self , "ip" )
2018-05-21 13:14:57 +00:00
return admin_info { "self" : admin_info { ip : self } } , nil
2018-02-07 23:48:30 +00:00
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "getPeers" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-20 16:21:14 +00:00
sort := "ip"
peers := make ( admin_info )
for _ , peerdata := range a . getData_getPeers ( ) {
p := peerdata . asMap ( )
so := fmt . Sprint ( p [ sort ] )
peers [ so ] = p
delete ( peers [ so ] . ( map [ string ] interface { } ) , sort )
}
return admin_info { "peers" : peers } , nil
2018-01-30 00:48:14 +00:00
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "getSwitchPeers" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-20 16:21:14 +00:00
sort := "port"
switchpeers := make ( admin_info )
for _ , s := range a . getData_getSwitchPeers ( ) {
p := s . asMap ( )
so := fmt . Sprint ( p [ sort ] )
switchpeers [ so ] = p
delete ( switchpeers [ so ] . ( map [ string ] interface { } ) , sort )
2018-02-28 13:43:06 +00:00
}
2018-05-20 16:21:14 +00:00
return admin_info { "switchpeers" : switchpeers } , nil
2018-02-28 13:43:06 +00:00
} )
2018-09-27 09:53:19 +00:00
a . addHandler ( "getSwitchQueues" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
queues := a . getData_getSwitchQueues ( )
return admin_info { "switchqueues" : queues . asMap ( ) } , nil
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "getDHT" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-20 16:21:14 +00:00
sort := "ip"
dht := make ( admin_info )
for _ , d := range a . getData_getDHT ( ) {
p := d . asMap ( )
so := fmt . Sprint ( p [ sort ] )
dht [ so ] = p
delete ( dht [ so ] . ( map [ string ] interface { } ) , sort )
2018-03-16 23:24:28 +00:00
}
2018-05-20 16:21:14 +00:00
return admin_info { "dht" : dht } , nil
2018-03-16 23:24:28 +00:00
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "getSessions" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
2018-05-20 16:21:14 +00:00
sort := "ip"
sessions := make ( admin_info )
for _ , s := range a . getData_getSessions ( ) {
p := s . asMap ( )
so := fmt . Sprint ( p [ sort ] )
sessions [ so ] = p
delete ( sessions [ so ] . ( map [ string ] interface { } ) , sort )
2018-05-10 08:48:12 +00:00
}
2018-05-20 16:21:14 +00:00
return admin_info { "sessions" : sessions } , nil
2018-05-10 08:48:12 +00:00
} )
2018-09-25 15:55:57 +00:00
a . addHandler ( "addPeer" , [ ] string { "uri" , "[interface]" } , func ( in admin_info ) ( admin_info , error ) {
2018-09-25 16:13:35 +00:00
// Set sane defaults
intf := ""
// Has interface been specified?
if itf , ok := in [ "interface" ] ; ok {
intf = itf . ( string )
}
if a . addPeer ( in [ "uri" ] . ( string ) , intf ) == nil {
2018-05-20 18:23:43 +00:00
return admin_info {
2018-05-20 20:54:15 +00:00
"added" : [ ] string {
2018-05-20 18:23:43 +00:00
in [ "uri" ] . ( string ) ,
} ,
} , nil
} else {
return admin_info {
2018-05-20 20:54:15 +00:00
"not_added" : [ ] string {
2018-05-20 18:23:43 +00:00
in [ "uri" ] . ( string ) ,
} ,
} , errors . New ( "Failed to add peer" )
}
} )
a . addHandler ( "removePeer" , [ ] string { "port" } , func ( in admin_info ) ( admin_info , error ) {
if a . removePeer ( fmt . Sprint ( in [ "port" ] ) ) == nil {
return admin_info {
2018-05-20 20:54:15 +00:00
"removed" : [ ] string {
2018-05-20 18:23:43 +00:00
fmt . Sprint ( in [ "port" ] ) ,
} ,
} , nil
} else {
return admin_info {
2018-05-20 20:54:15 +00:00
"not_removed" : [ ] string {
2018-05-20 18:23:43 +00:00
fmt . Sprint ( in [ "port" ] ) ,
} ,
} , errors . New ( "Failed to remove peer" )
}
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "getTunTap" , [ ] string { } , func ( in admin_info ) ( r admin_info , e error ) {
2018-05-20 18:42:37 +00:00
defer func ( ) {
recover ( )
2018-05-21 13:14:57 +00:00
r = admin_info { "none" : admin_info { } }
2018-05-20 18:42:37 +00:00
e = nil
} ( )
2018-05-20 16:21:14 +00:00
2018-05-20 18:42:37 +00:00
return admin_info {
2018-05-21 12:54:51 +00:00
a . core . tun . iface . Name ( ) : admin_info {
"tap_mode" : a . core . tun . iface . IsTAP ( ) ,
"mtu" : a . core . tun . mtu ,
} ,
2018-05-20 18:42:37 +00:00
} , nil
} )
2018-05-20 20:57:05 +00:00
a . addHandler ( "setTunTap" , [ ] string { "name" , "[tap_mode]" , "[mtu]" } , func ( in admin_info ) ( admin_info , error ) {
2018-05-20 20:44:30 +00:00
// Set sane defaults
2018-07-07 11:08:52 +00:00
iftapmode := defaults . GetDefaults ( ) . DefaultIfTAPMode
ifmtu := defaults . GetDefaults ( ) . DefaultIfMTU
2018-05-20 20:44:30 +00:00
// Has TAP mode been specified?
if tap , ok := in [ "tap_mode" ] ; ok {
iftapmode = tap . ( bool )
}
// Check we have enough params for MTU
if mtu , ok := in [ "mtu" ] ; ok {
2018-07-07 11:08:52 +00:00
if mtu . ( float64 ) >= 1280 && ifmtu <= defaults . GetDefaults ( ) . MaximumIfMTU {
2018-05-20 20:54:15 +00:00
ifmtu = int ( in [ "mtu" ] . ( float64 ) )
2018-05-20 16:21:14 +00:00
}
2018-05-20 20:44:30 +00:00
}
// Start the TUN adapter
if err := a . startTunWithMTU ( in [ "name" ] . ( string ) , iftapmode , ifmtu ) ; err != nil {
return admin_info { } , errors . New ( "Failed to configure adapter" )
} else {
return admin_info {
2018-05-21 12:54:51 +00:00
a . core . tun . iface . Name ( ) : admin_info {
"tap_mode" : a . core . tun . iface . IsTAP ( ) ,
"mtu" : ifmtu ,
} ,
2018-05-20 20:44:30 +00:00
} , nil
}
} )
2018-05-23 21:13:52 +00:00
a . addHandler ( "getMulticastInterfaces" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
var intfs [ ] string
2018-06-08 03:07:19 +00:00
for _ , v := range a . core . multicast . interfaces ( ) {
2018-05-23 21:13:52 +00:00
intfs = append ( intfs , v . Name )
}
return admin_info { "multicast_interfaces" : intfs } , nil
} )
2018-05-23 10:28:20 +00:00
a . addHandler ( "getAllowedEncryptionPublicKeys" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
return admin_info { "allowed_box_pubs" : a . getAllowedEncryptionPublicKeys ( ) } , nil
2018-05-20 20:44:30 +00:00
} )
2018-11-26 17:34:26 +00:00
a . addHandler ( "addAllowedEncryptionPublicKey" , [ ] string { "box_pub_key" } , func ( in admin_info ) ( admin_info , error ) {
if a . addAllowedEncryptionPublicKey ( in [ "box_pub_key" ] . ( string ) ) == nil {
2018-05-20 20:54:15 +00:00
return admin_info {
"added" : [ ] string {
2018-11-26 17:34:26 +00:00
in [ "box_pub_key" ] . ( string ) ,
2018-05-20 20:54:15 +00:00
} ,
} , nil
} else {
return admin_info {
"not_added" : [ ] string {
2018-11-26 17:34:26 +00:00
in [ "box_pub_key" ] . ( string ) ,
2018-05-20 20:54:15 +00:00
} ,
2018-06-22 21:26:17 +00:00
} , errors . New ( "Failed to add allowed key" )
2018-05-20 20:54:15 +00:00
}
} )
2018-11-26 17:34:26 +00:00
a . addHandler ( "removeAllowedEncryptionPublicKey" , [ ] string { "box_pub_key" } , func ( in admin_info ) ( admin_info , error ) {
if a . removeAllowedEncryptionPublicKey ( in [ "box_pub_key" ] . ( string ) ) == nil {
2018-05-20 20:54:15 +00:00
return admin_info {
"removed" : [ ] string {
2018-11-26 17:34:26 +00:00
in [ "box_pub_key" ] . ( string ) ,
2018-05-20 20:54:15 +00:00
} ,
} , nil
} else {
return admin_info {
"not_removed" : [ ] string {
2018-11-26 17:34:26 +00:00
in [ "box_pub_key" ] . ( string ) ,
2018-05-20 20:54:15 +00:00
} ,
2018-06-22 21:26:17 +00:00
} , errors . New ( "Failed to remove allowed key" )
2018-05-20 20:54:15 +00:00
}
} )
2018-11-21 06:10:20 +00:00
a . addHandler ( "addSourceSubnet" , [ ] string { "subnet" } , func ( in admin_info ) ( admin_info , error ) {
var err error
a . core . router . doAdmin ( func ( ) {
err = a . core . router . cryptokey . addSourceSubnet ( in [ "subnet" ] . ( string ) )
} )
if err == nil {
return admin_info { "added" : [ ] string { in [ "subnet" ] . ( string ) } } , nil
} else {
return admin_info { "not_added" : [ ] string { in [ "subnet" ] . ( string ) } } , errors . New ( "Failed to add source subnet" )
}
} )
2018-11-26 17:58:54 +00:00
a . addHandler ( "addRoute" , [ ] string { "subnet" , "box_pub_key" } , func ( in admin_info ) ( admin_info , error ) {
2018-11-21 06:10:20 +00:00
var err error
a . core . router . doAdmin ( func ( ) {
2018-11-26 17:58:54 +00:00
err = a . core . router . cryptokey . addRoute ( in [ "subnet" ] . ( string ) , in [ "box_pub_key" ] . ( string ) )
2018-11-21 06:10:20 +00:00
} )
if err == nil {
2018-11-26 17:58:54 +00:00
return admin_info { "added" : [ ] string { fmt . Sprintf ( "%s via %s" , in [ "subnet" ] . ( string ) , in [ "box_pub_key" ] . ( string ) ) } } , nil
2018-11-21 06:10:20 +00:00
} else {
2018-11-26 17:58:54 +00:00
return admin_info { "not_added" : [ ] string { fmt . Sprintf ( "%s via %s" , in [ "subnet" ] . ( string ) , in [ "box_pub_key" ] . ( string ) ) } } , errors . New ( "Failed to add route" )
2018-11-21 06:10:20 +00:00
}
} )
a . addHandler ( "getSourceSubnets" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
var subnets [ ] string
a . core . router . doAdmin ( func ( ) {
getSourceSubnets := func ( snets [ ] net . IPNet ) {
for _ , subnet := range snets {
subnets = append ( subnets , subnet . String ( ) )
}
}
getSourceSubnets ( a . core . router . cryptokey . ipv4sources )
getSourceSubnets ( a . core . router . cryptokey . ipv6sources )
} )
return admin_info { "source_subnets" : subnets } , nil
} )
a . addHandler ( "getRoutes" , [ ] string { } , func ( in admin_info ) ( admin_info , error ) {
var routes [ ] string
a . core . router . doAdmin ( func ( ) {
getRoutes := func ( ckrs [ ] cryptokey_route ) {
for _ , ckr := range ckrs {
routes = append ( routes , fmt . Sprintf ( "%s via %s" , ckr . subnet . String ( ) , hex . EncodeToString ( ckr . destination [ : ] ) ) )
}
}
getRoutes ( a . core . router . cryptokey . ipv4routes )
getRoutes ( a . core . router . cryptokey . ipv6routes )
} )
return admin_info { "routes" : routes } , nil
} )
2018-11-23 03:30:56 +00:00
a . addHandler ( "removeSourceSubnet" , [ ] string { "subnet" } , func ( in admin_info ) ( admin_info , error ) {
var err error
a . core . router . doAdmin ( func ( ) {
err = a . core . router . cryptokey . removeSourceSubnet ( in [ "subnet" ] . ( string ) )
} )
if err == nil {
return admin_info { "removed" : [ ] string { in [ "subnet" ] . ( string ) } } , nil
} else {
return admin_info { "not_removed" : [ ] string { in [ "subnet" ] . ( string ) } } , errors . New ( "Failed to remove source subnet" )
}
} )
2018-11-26 17:58:54 +00:00
a . addHandler ( "removeRoute" , [ ] string { "subnet" , "box_pub_key" } , func ( in admin_info ) ( admin_info , error ) {
2018-11-23 03:30:56 +00:00
var err error
a . core . router . doAdmin ( func ( ) {
2018-11-26 17:58:54 +00:00
err = a . core . router . cryptokey . removeRoute ( in [ "subnet" ] . ( string ) , in [ "box_pub_key" ] . ( string ) )
2018-11-23 03:30:56 +00:00
} )
if err == nil {
2018-11-26 17:58:54 +00:00
return admin_info { "removed" : [ ] string { fmt . Sprintf ( "%s via %s" , in [ "subnet" ] . ( string ) , in [ "box_pub_key" ] . ( string ) ) } } , nil
2018-11-23 03:30:56 +00:00
} else {
2018-11-26 17:58:54 +00:00
return admin_info { "not_removed" : [ ] string { fmt . Sprintf ( "%s via %s" , in [ "subnet" ] . ( string ) , in [ "box_pub_key" ] . ( string ) ) } } , errors . New ( "Failed to remove route" )
2018-11-23 03:30:56 +00:00
}
} )
2018-11-26 17:34:26 +00:00
a . addHandler ( "dhtPing" , [ ] string { "box_pub_key" , "coords" , "[target]" } , func ( in admin_info ) ( admin_info , error ) {
2018-11-25 22:10:32 +00:00
if in [ "target" ] == nil {
in [ "target" ] = "none"
}
2018-11-26 17:34:26 +00:00
result , err := a . admin_dhtPing ( in [ "box_pub_key" ] . ( string ) , in [ "coords" ] . ( string ) , in [ "target" ] . ( string ) )
2018-11-25 22:10:32 +00:00
if err == nil {
2018-11-26 00:25:31 +00:00
infos := make ( map [ string ] map [ string ] string , len ( result . Infos ) )
2018-11-25 22:10:32 +00:00
for _ , dinfo := range result . Infos {
2018-11-25 22:16:06 +00:00
info := map [ string ] string {
2018-11-26 17:34:26 +00:00
"box_pub_key" : hex . EncodeToString ( dinfo . key [ : ] ) ,
"coords" : fmt . Sprintf ( "%v" , dinfo . coords ) ,
2018-11-25 22:16:06 +00:00
}
2018-11-26 00:25:31 +00:00
addr := net . IP ( address_addrForNodeID ( getNodeID ( & dinfo . key ) ) [ : ] ) . String ( )
infos [ addr ] = info
2018-11-25 22:10:32 +00:00
}
return admin_info { "nodes" : infos } , nil
} else {
return admin_info { } , err
}
} )
2018-05-27 21:13:37 +00:00
}
2018-06-10 23:03:28 +00:00
// start runs the admin API socket to listen for / respond to admin API calls.
2018-05-27 21:13:37 +00:00
func ( a * admin ) start ( ) error {
2018-01-21 00:17:15 +00:00
go a . listen ( )
2018-05-27 21:13:37 +00:00
return nil
2018-01-21 00:17:15 +00:00
}
2018-07-07 11:34:10 +00:00
// cleans up when stopping
2018-07-07 19:04:11 +00:00
func ( a * admin ) close ( ) error {
return a . listener . Close ( )
2018-07-07 11:34:10 +00:00
}
2018-06-10 23:03:28 +00:00
// listen is run by start and manages API connections.
2018-01-21 00:17:15 +00:00
func ( a * admin ) listen ( ) {
2018-07-07 10:22:49 +00:00
u , err := url . Parse ( a . listenaddr )
if err == nil {
switch strings . ToLower ( u . Scheme ) {
case "unix" :
2018-07-07 19:04:11 +00:00
a . listener , err = net . Listen ( "unix" , a . listenaddr [ 7 : ] )
2018-07-07 10:22:49 +00:00
case "tcp" :
2018-07-07 19:04:11 +00:00
a . listener , err = net . Listen ( "tcp" , u . Host )
2018-07-07 10:22:49 +00:00
default :
2018-07-08 09:37:20 +00:00
// err = errors.New(fmt.Sprint("protocol not supported: ", u.Scheme))
a . listener , err = net . Listen ( "tcp" , a . listenaddr )
2018-07-07 10:22:49 +00:00
}
} else {
2018-07-07 19:04:11 +00:00
a . listener , err = net . Listen ( "tcp" , a . listenaddr )
2018-07-07 10:22:49 +00:00
}
2018-01-21 00:17:15 +00:00
if err != nil {
a . core . log . Printf ( "Admin socket failed to listen: %v" , err )
os . Exit ( 1 )
}
2018-07-07 19:04:11 +00:00
a . core . log . Printf ( "%s admin socket listening on %s" ,
strings . ToUpper ( a . listener . Addr ( ) . Network ( ) ) ,
a . listener . Addr ( ) . String ( ) )
defer a . listener . Close ( )
2018-01-21 00:17:15 +00:00
for {
2018-07-07 19:04:11 +00:00
conn , err := a . listener . Accept ( )
2018-01-21 00:17:15 +00:00
if err == nil {
a . handleRequest ( conn )
}
}
}
2018-06-10 23:03:28 +00:00
// handleRequest calls the request handler for each request sent to the admin API.
2018-01-21 00:17:15 +00:00
func ( a * admin ) handleRequest ( conn net . Conn ) {
2018-05-20 16:21:14 +00:00
decoder := json . NewDecoder ( conn )
encoder := json . NewEncoder ( conn )
encoder . SetIndent ( "" , " " )
recv := make ( admin_info )
send := make ( admin_info )
2018-05-20 20:44:30 +00:00
defer func ( ) {
r := recover ( )
if r != nil {
send = admin_info {
"status" : "error" ,
"error" : "Unrecoverable error, possibly as a result of invalid input types or malformed syntax" ,
}
2018-05-20 20:54:15 +00:00
fmt . Println ( "Admin socket error:" , r )
2018-05-20 20:44:30 +00:00
if err := encoder . Encode ( & send ) ; err != nil {
fmt . Println ( "Admin socket JSON encode error:" , err )
}
conn . Close ( )
}
} ( )
2018-05-20 16:21:14 +00:00
for {
2018-05-20 20:44:30 +00:00
// Start with a clean slate on each request
recv = admin_info { }
send = admin_info { }
// Decode the input
2018-05-20 16:21:14 +00:00
if err := decoder . Decode ( & recv ) ; err != nil {
2018-05-21 06:28:03 +00:00
// fmt.Println("Admin socket JSON decode error:", err)
2018-05-20 16:21:14 +00:00
return
2018-01-21 12:57:54 +00:00
}
2018-05-20 20:44:30 +00:00
// Send the request back with the response, and default to "error"
// unless the status is changed below by one of the handlers
2018-05-20 18:23:43 +00:00
send [ "request" ] = recv
send [ "status" ] = "error"
2018-05-20 16:21:14 +00:00
handlers :
for _ , handler := range a . handlers {
2018-05-20 20:44:30 +00:00
// We've found the handler that matches the request
2018-07-09 18:30:41 +00:00
if strings . ToLower ( recv [ "request" ] . ( string ) ) == strings . ToLower ( handler . name ) {
2018-05-20 16:21:14 +00:00
// Check that we have all the required arguments
for _ , arg := range handler . args {
2018-05-20 20:57:05 +00:00
// An argument in [square brackets] is optional and not required,
2018-05-20 16:21:14 +00:00
// so we can safely ignore those
2018-05-20 20:57:05 +00:00
if strings . HasPrefix ( arg , "[" ) && strings . HasSuffix ( arg , "]" ) {
2018-05-20 16:21:14 +00:00
continue
}
// Check if the field is missing
if _ , ok := recv [ arg ] ; ! ok {
send = admin_info {
2018-05-20 22:25:07 +00:00
"status" : "error" ,
2018-05-21 13:14:57 +00:00
"error" : "Expected field missing: " + arg ,
2018-05-20 20:44:30 +00:00
"expecting" : arg ,
2018-05-20 16:21:14 +00:00
}
break handlers
}
}
// By this point we should have all the fields we need, so call
// the handler
response , err := handler . handler ( recv )
if err != nil {
2018-05-20 18:23:43 +00:00
send [ "error" ] = err . Error ( )
if response != nil {
send [ "response" ] = response
2018-05-20 16:21:14 +00:00
}
} else {
2018-05-20 18:23:43 +00:00
send [ "status" ] = "success"
if response != nil {
send [ "response" ] = response
2018-05-20 16:21:14 +00:00
}
}
2018-05-20 20:44:30 +00:00
2018-05-20 16:21:14 +00:00
break
}
}
2018-05-20 20:44:30 +00:00
// Send the response back
2018-05-20 16:21:14 +00:00
if err := encoder . Encode ( & send ) ; err != nil {
return
}
2018-05-20 20:44:30 +00:00
// If "keepalive" isn't true then close the connection
if keepalive , ok := recv [ "keepalive" ] ; ! ok || ! keepalive . ( bool ) {
conn . Close ( )
}
2018-05-20 16:21:14 +00:00
}
2018-01-30 00:48:14 +00:00
}
2018-01-21 18:55:45 +00:00
2018-06-10 23:03:28 +00:00
// asMap converts an admin_nodeInfo into a map of key/value pairs.
2018-05-20 16:21:14 +00:00
func ( n * admin_nodeInfo ) asMap ( ) map [ string ] interface { } {
m := make ( map [ string ] interface { } , len ( * n ) )
2018-01-30 00:48:14 +00:00
for _ , p := range * n {
m [ p . key ] = p . val
}
return m
}
2018-06-10 23:03:28 +00:00
// toString creates a printable string representation of an admin_nodeInfo.
2018-01-30 00:48:14 +00:00
func ( n * admin_nodeInfo ) toString ( ) string {
// TODO return something nicer looking than this
var out [ ] string
for _ , p := range * n {
out = append ( out , fmt . Sprintf ( "%v: %v" , p . key , p . val ) )
}
return strings . Join ( out , ", " )
}
2018-06-10 23:03:28 +00:00
// printInfos returns a newline separated list of strings from admin_nodeInfos, e.g. a printable string of info about all peers.
2018-01-30 00:48:14 +00:00
func ( a * admin ) printInfos ( infos [ ] admin_nodeInfo ) string {
var out [ ] string
for _ , info := range infos {
out = append ( out , info . toString ( ) )
}
out = append ( out , "" ) // To add a trailing "\n" in the join
return strings . Join ( out , "\n" )
}
2018-01-21 12:57:54 +00:00
2018-06-10 23:03:28 +00:00
// addPeer triggers a connection attempt to a node.
2018-09-25 15:55:57 +00:00
func ( a * admin ) addPeer ( addr string , sintf string ) error {
2018-05-05 22:14:03 +00:00
u , err := url . Parse ( addr )
if err == nil {
switch strings . ToLower ( u . Scheme ) {
case "tcp" :
2018-09-25 15:55:57 +00:00
a . core . tcp . connect ( u . Host , sintf )
2018-05-05 22:14:03 +00:00
case "socks" :
2018-05-27 21:13:37 +00:00
a . core . tcp . connectSOCKS ( u . Host , u . Path [ 1 : ] )
2018-05-05 22:14:03 +00:00
default :
return errors . New ( "invalid peer: " + addr )
2018-02-28 13:43:06 +00:00
}
2018-05-05 22:14:03 +00:00
} else {
// no url scheme provided
addr = strings . ToLower ( addr )
2018-06-06 21:40:35 +00:00
if strings . HasPrefix ( addr , "tcp:" ) {
addr = addr [ 4 : ]
2018-02-28 13:43:06 +00:00
}
2018-09-25 14:32:45 +00:00
a . core . tcp . connect ( addr , "" )
2018-06-06 21:40:35 +00:00
return nil
2018-02-28 13:43:06 +00:00
}
return nil
}
2018-06-10 23:03:28 +00:00
// removePeer disconnects an existing node (given by the node's port number).
2018-03-16 23:24:28 +00:00
func ( a * admin ) removePeer ( p string ) error {
2018-05-05 22:14:03 +00:00
iport , err := strconv . Atoi ( p )
if err != nil {
return err
2018-03-16 23:24:28 +00:00
}
2018-05-05 22:14:03 +00:00
a . core . peers . removePeer ( switchPort ( iport ) )
2018-03-16 23:24:28 +00:00
return nil
}
2018-06-10 23:03:28 +00:00
// startTunWithMTU creates the tun/tap device, sets its address, and sets the MTU to the provided value.
2018-02-28 15:15:57 +00:00
func ( a * admin ) startTunWithMTU ( ifname string , iftapmode bool , ifmtu int ) error {
// Close the TUN first if open
_ = a . core . tun . close ( )
// Then reconfigure and start it
addr := a . core . router . addr
2018-06-14 12:08:48 +00:00
straddr := fmt . Sprintf ( "%s/%v" , net . IP ( addr [ : ] ) . String ( ) , 8 * len ( address_prefix ) - 1 )
2018-02-28 15:15:57 +00:00
if ifname != "none" {
err := a . core . tun . setup ( ifname , iftapmode , straddr , ifmtu )
if err != nil {
return err
}
2018-05-18 17:56:33 +00:00
// If we have open sessions then we need to notify them
// that our MTU has now changed
for _ , sinfo := range a . core . sessions . sinfos {
if ifname == "none" {
sinfo . myMTU = 0
} else {
sinfo . myMTU = uint16 ( ifmtu )
}
a . core . sessions . sendPingPong ( sinfo , false )
}
// Aaaaand... go!
2018-02-28 15:15:57 +00:00
go a . core . tun . read ( )
}
go a . core . tun . write ( )
return nil
}
2018-06-10 23:03:28 +00:00
// getData_getSelf returns the self node's info for admin responses.
2018-01-30 00:48:14 +00:00
func ( a * admin ) getData_getSelf ( ) * admin_nodeInfo {
table := a . core . switchTable . table . Load ( ) . ( lookupTable )
coords := table . self . getCoords ( )
self := admin_nodeInfo {
2018-11-26 17:34:26 +00:00
{ "box_pub_key" , hex . EncodeToString ( a . core . boxPub [ : ] ) } ,
2018-05-27 21:13:37 +00:00
{ "ip" , a . core . GetAddress ( ) . String ( ) } ,
{ "subnet" , a . core . GetSubnet ( ) . String ( ) } ,
2018-01-30 00:48:14 +00:00
{ "coords" , fmt . Sprint ( coords ) } ,
}
return & self
}
2018-06-10 23:03:28 +00:00
// getData_getPeers returns info from Core.peers for an admin response.
2018-01-30 00:48:14 +00:00
func ( a * admin ) getData_getPeers ( ) [ ] admin_nodeInfo {
2018-02-07 23:48:30 +00:00
ports := a . core . peers . ports . Load ( ) . ( map [ switchPort ] * peer )
var peerInfos [ ] admin_nodeInfo
var ps [ ] switchPort
for port := range ports {
ps = append ( ps , port )
}
sort . Slice ( ps , func ( i , j int ) bool { return ps [ i ] < ps [ j ] } )
for _ , port := range ps {
p := ports [ port ]
addr := * address_addrForNodeID ( getNodeID ( & p . box ) )
info := admin_nodeInfo {
2018-05-20 16:21:14 +00:00
{ "ip" , net . IP ( addr [ : ] ) . String ( ) } ,
{ "port" , port } ,
2018-05-21 14:21:23 +00:00
{ "uptime" , int ( time . Since ( p . firstSeen ) . Seconds ( ) ) } ,
{ "bytes_sent" , atomic . LoadUint64 ( & p . bytesSent ) } ,
{ "bytes_recvd" , atomic . LoadUint64 ( & p . bytesRecvd ) } ,
2018-10-21 16:57:48 +00:00
{ "endpoint" , p . endpoint } ,
2018-11-26 17:38:02 +00:00
{ "box_pub_key" , hex . EncodeToString ( p . box [ : ] ) } ,
2018-02-07 23:48:30 +00:00
}
peerInfos = append ( peerInfos , info )
}
return peerInfos
}
2018-06-10 23:03:28 +00:00
// getData_getSwitchPeers returns info from Core.switchTable for an admin response.
2018-02-07 23:48:30 +00:00
func ( a * admin ) getData_getSwitchPeers ( ) [ ] admin_nodeInfo {
2018-01-30 00:48:14 +00:00
var peerInfos [ ] admin_nodeInfo
table := a . core . switchTable . table . Load ( ) . ( lookupTable )
peers := a . core . peers . ports . Load ( ) . ( map [ switchPort ] * peer )
for _ , elem := range table . elems {
peer , isIn := peers [ elem . port ]
if ! isIn {
continue
2018-01-21 20:58:54 +00:00
}
2018-01-30 00:48:14 +00:00
addr := * address_addrForNodeID ( getNodeID ( & peer . box ) )
coords := elem . locator . getCoords ( )
info := admin_nodeInfo {
2018-05-20 16:21:14 +00:00
{ "ip" , net . IP ( addr [ : ] ) . String ( ) } ,
2018-01-30 00:48:14 +00:00
{ "coords" , fmt . Sprint ( coords ) } ,
2018-05-20 16:21:14 +00:00
{ "port" , elem . port } ,
2018-09-25 16:59:01 +00:00
{ "bytes_sent" , atomic . LoadUint64 ( & peer . bytesSent ) } ,
{ "bytes_recvd" , atomic . LoadUint64 ( & peer . bytesRecvd ) } ,
2018-10-21 16:57:48 +00:00
{ "endpoint" , peer . endpoint } ,
2018-11-26 17:38:02 +00:00
{ "box_pub_key" , hex . EncodeToString ( peer . box [ : ] ) } ,
2018-01-21 20:58:54 +00:00
}
2018-01-30 00:48:14 +00:00
peerInfos = append ( peerInfos , info )
}
return peerInfos
}
2018-09-27 13:20:52 +00:00
// getData_getSwitchQueues returns info from Core.switchTable for an queue data.
2018-09-27 09:53:19 +00:00
func ( a * admin ) getData_getSwitchQueues ( ) admin_nodeInfo {
var peerInfos admin_nodeInfo
2018-11-23 03:41:16 +00:00
switchTable := & a . core . switchTable
2018-09-27 09:53:19 +00:00
getSwitchQueues := func ( ) {
2018-09-27 11:14:55 +00:00
queues := make ( [ ] map [ string ] interface { } , 0 )
for k , v := range switchTable . queues . bufs {
2018-09-27 14:05:45 +00:00
nexthop := switchTable . bestPortForCoords ( [ ] byte ( k ) )
2018-09-27 11:14:55 +00:00
queue := map [ string ] interface { } {
"queue_id" : k ,
"queue_size" : v . size ,
"queue_packets" : len ( v . packets ) ,
2018-09-27 14:05:45 +00:00
"queue_port" : nexthop ,
2018-09-27 11:14:55 +00:00
}
queues = append ( queues , queue )
}
2018-09-27 09:53:19 +00:00
peerInfos = admin_nodeInfo {
2018-09-27 11:14:55 +00:00
{ "queues" , queues } ,
2018-09-27 09:53:19 +00:00
{ "queues_count" , len ( switchTable . queues . bufs ) } ,
{ "queues_size" , switchTable . queues . size } ,
2018-09-27 15:19:47 +00:00
{ "highest_queues_count" , switchTable . queues . maxbufs } ,
{ "highest_queues_size" , switchTable . queues . maxsize } ,
{ "maximum_queues_size" , switch_buffer_maxSize } ,
2018-09-27 09:53:19 +00:00
}
}
a . core . switchTable . doAdmin ( getSwitchQueues )
return peerInfos
}
2018-06-10 23:03:28 +00:00
// getData_getDHT returns info from Core.dht for an admin response.
2018-01-30 00:48:14 +00:00
func ( a * admin ) getData_getDHT ( ) [ ] admin_nodeInfo {
var infos [ ] admin_nodeInfo
getDHT := func ( ) {
2018-10-20 22:58:54 +00:00
now := time . Now ( )
var dhtInfos [ ] * dhtInfo
for _ , v := range a . core . dht . table {
dhtInfos = append ( dhtInfos , v )
}
sort . SliceStable ( dhtInfos , func ( i , j int ) bool {
return dht_ordered ( & a . core . dht . nodeID , dhtInfos [ i ] . getNodeID ( ) , dhtInfos [ j ] . getNodeID ( ) )
} )
for _ , v := range dhtInfos {
addr := * address_addrForNodeID ( v . getNodeID ( ) )
info := admin_nodeInfo {
{ "ip" , net . IP ( addr [ : ] ) . String ( ) } ,
{ "coords" , fmt . Sprint ( v . coords ) } ,
{ "last_seen" , int ( now . Sub ( v . recv ) . Seconds ( ) ) } ,
2018-11-26 17:38:02 +00:00
{ "box_pub_key" , hex . EncodeToString ( v . key [ : ] ) } ,
2018-01-21 20:58:54 +00:00
}
2018-10-20 22:58:54 +00:00
infos = append ( infos , info )
2018-01-21 20:58:54 +00:00
}
2018-01-30 00:48:14 +00:00
}
a . core . router . doAdmin ( getDHT )
return infos
}
2018-06-10 23:03:28 +00:00
// getData_getSessions returns info from Core.sessions for an admin response.
2018-01-30 00:48:14 +00:00
func ( a * admin ) getData_getSessions ( ) [ ] admin_nodeInfo {
var infos [ ] admin_nodeInfo
getSessions := func ( ) {
for _ , sinfo := range a . core . sessions . sinfos {
// TODO? skipped known but timed out sessions?
info := admin_nodeInfo {
2018-05-20 16:21:14 +00:00
{ "ip" , net . IP ( sinfo . theirAddr [ : ] ) . String ( ) } ,
2018-01-30 00:48:14 +00:00
{ "coords" , fmt . Sprint ( sinfo . coords ) } ,
2018-05-20 16:21:14 +00:00
{ "mtu" , sinfo . getMTU ( ) } ,
{ "was_mtu_fixed" , sinfo . wasMTUFixed } ,
2018-05-21 14:21:23 +00:00
{ "bytes_sent" , sinfo . bytesSent } ,
{ "bytes_recvd" , sinfo . bytesRecvd } ,
2018-11-26 17:38:02 +00:00
{ "box_pub_key" , hex . EncodeToString ( sinfo . theirPermPub [ : ] ) } ,
2018-01-21 12:57:54 +00:00
}
2018-01-30 00:48:14 +00:00
infos = append ( infos , info )
2018-01-21 20:58:54 +00:00
}
2018-01-30 00:48:14 +00:00
}
a . core . router . doAdmin ( getSessions )
return infos
}
2018-06-10 23:03:28 +00:00
// getAllowedEncryptionPublicKeys returns the public keys permitted for incoming peer connections.
2018-05-23 10:28:20 +00:00
func ( a * admin ) getAllowedEncryptionPublicKeys ( ) [ ] string {
pubs := a . core . peers . getAllowedEncryptionPublicKeys ( )
2018-05-07 00:31:19 +00:00
var out [ ] string
for _ , pub := range pubs {
out = append ( out , hex . EncodeToString ( pub [ : ] ) )
}
2018-05-20 20:44:30 +00:00
return out
2018-05-07 00:31:19 +00:00
}
2018-06-10 23:03:28 +00:00
// addAllowedEncryptionPublicKey whitelists a key for incoming peer connections.
2018-05-23 10:28:20 +00:00
func ( a * admin ) addAllowedEncryptionPublicKey ( bstr string ) ( err error ) {
2018-05-07 00:31:19 +00:00
boxBytes , err := hex . DecodeString ( bstr )
2018-05-07 00:48:26 +00:00
if err == nil {
2018-05-07 00:31:19 +00:00
var box boxPubKey
copy ( box [ : ] , boxBytes )
2018-05-23 10:28:20 +00:00
a . core . peers . addAllowedEncryptionPublicKey ( & box )
2018-05-07 00:31:19 +00:00
}
return
}
2018-06-10 23:03:28 +00:00
// removeAllowedEncryptionPublicKey removes a key from the whitelist for incoming peer connections.
// If none are set, an empty list permits all incoming connections.
2018-05-23 10:28:20 +00:00
func ( a * admin ) removeAllowedEncryptionPublicKey ( bstr string ) ( err error ) {
2018-05-07 00:31:19 +00:00
boxBytes , err := hex . DecodeString ( bstr )
2018-05-07 00:48:26 +00:00
if err == nil {
2018-05-07 00:31:19 +00:00
var box boxPubKey
copy ( box [ : ] , boxBytes )
2018-05-23 10:28:20 +00:00
a . core . peers . removeAllowedEncryptionPublicKey ( & box )
2018-05-07 00:31:19 +00:00
}
return
}
2018-11-25 22:10:32 +00:00
// Send a DHT ping to the node with the provided key and coords, optionally looking up the specified target NodeID.
func ( a * admin ) admin_dhtPing ( keyString , coordString , targetString string ) ( dhtRes , error ) {
var key boxPubKey
if keyBytes , err := hex . DecodeString ( keyString ) ; err != nil {
return dhtRes { } , err
} else {
copy ( key [ : ] , keyBytes )
}
var coords [ ] byte
for _ , cstr := range strings . Split ( strings . Trim ( coordString , "[]" ) , " " ) {
if u64 , err := strconv . ParseUint ( cstr , 10 , 8 ) ; err != nil {
return dhtRes { } , err
} else {
coords = append ( coords , uint8 ( u64 ) )
}
}
2018-11-25 23:59:36 +00:00
resCh := make ( chan * dhtRes , 1 )
2018-11-25 22:10:32 +00:00
info := dhtInfo {
key : key ,
coords : coords ,
}
target := * info . getNodeID ( )
if targetString == "none" {
// Leave the default target in place
} else if targetBytes , err := hex . DecodeString ( targetString ) ; err != nil {
return dhtRes { } , err
} else if len ( targetBytes ) != len ( target ) {
return dhtRes { } , errors . New ( "Incorrect target NodeID length" )
} else {
target = NodeID { }
copy ( target [ : ] , targetBytes )
}
rq := dhtReqKey { info . key , target }
sendPing := func ( ) {
a . core . dht . addCallback ( & rq , func ( res * dhtRes ) {
defer func ( ) { recover ( ) } ( )
select {
case resCh <- res :
default :
}
} )
a . core . dht . ping ( & info , & target )
}
a . core . router . doAdmin ( sendPing )
go func ( ) {
time . Sleep ( 6 * time . Second )
close ( resCh )
} ( )
for res := range resCh {
return * res , nil
}
return dhtRes { } , errors . New ( fmt . Sprintf ( "DHT ping timeout: %s" , keyString ) )
}
2018-06-10 23:03:28 +00:00
// getResponse_dot returns a response for a graphviz dot formatted representation of the known parts of the network.
// This is color-coded and labeled, and includes the self node, switch peers, nodes known to the DHT, and nodes with open sessions.
// The graph is structured as a tree with directed links leading away from the root.
2018-01-30 00:48:14 +00:00
func ( a * admin ) getResponse_dot ( ) [ ] byte {
2018-06-01 01:28:09 +00:00
self := a . getData_getSelf ( )
2018-02-07 23:48:30 +00:00
peers := a . getData_getSwitchPeers ( )
2018-01-30 00:48:14 +00:00
dht := a . getData_getDHT ( )
sessions := a . getData_getSessions ( )
// Start building a tree from all known nodes
type nodeInfo struct {
2018-06-01 01:28:09 +00:00
name string
key string
parent string
2018-07-21 04:02:25 +00:00
port switchPort
2018-06-01 01:28:09 +00:00
options string
2018-01-30 00:48:14 +00:00
}
infos := make ( map [ string ] nodeInfo )
2018-07-21 04:02:25 +00:00
// Get coords as a slice of strings, FIXME? this looks very fragile
coordSlice := func ( coords string ) [ ] string {
tmp := strings . Replace ( coords , "[" , "" , - 1 )
tmp = strings . Replace ( tmp , "]" , "" , - 1 )
return strings . Split ( tmp , " " )
}
2018-01-30 00:48:14 +00:00
// First fill the tree with all known nodes, no parents
2018-06-01 22:23:24 +00:00
addInfo := func ( nodes [ ] admin_nodeInfo , options string , tag string ) {
2018-06-01 01:28:09 +00:00
for _ , node := range nodes {
n := node . asMap ( )
info := nodeInfo {
key : n [ "coords" ] . ( string ) ,
options : options ,
}
2018-06-01 22:23:24 +00:00
if len ( tag ) > 0 {
info . name = fmt . Sprintf ( "%s\n%s" , n [ "ip" ] . ( string ) , tag )
} else {
info . name = n [ "ip" ] . ( string )
}
2018-07-21 04:02:25 +00:00
coordsSplit := coordSlice ( info . key )
if len ( coordsSplit ) != 0 {
portStr := coordsSplit [ len ( coordsSplit ) - 1 ]
portUint , err := strconv . ParseUint ( portStr , 10 , 64 )
if err == nil {
info . port = switchPort ( portUint )
}
}
2018-06-01 01:28:09 +00:00
infos [ info . key ] = info
2018-01-21 22:19:39 +00:00
}
2018-01-30 00:48:14 +00:00
}
2018-06-02 20:21:05 +00:00
addInfo ( dht , "fillcolor=\"#ffffff\" style=filled fontname=\"sans serif\"" , "Known in DHT" ) // white
addInfo ( sessions , "fillcolor=\"#acf3fd\" style=filled fontname=\"sans serif\"" , "Open session" ) // blue
addInfo ( peers , "fillcolor=\"#ffffb5\" style=filled fontname=\"sans serif\"" , "Connected peer" ) // yellow
2018-06-02 13:24:06 +00:00
addInfo ( append ( [ ] admin_nodeInfo ( nil ) , * self ) , "fillcolor=\"#a5ff8a\" style=filled fontname=\"sans serif\"" , "This node" ) // green
2018-01-30 00:48:14 +00:00
// Now go through and create placeholders for any missing nodes
for _ , info := range infos {
// This is ugly string manipulation
coordsSplit := coordSlice ( info . key )
for idx := range coordsSplit {
key := fmt . Sprintf ( "[%v]" , strings . Join ( coordsSplit [ : idx ] , " " ) )
newInfo , isIn := infos [ key ]
if isIn {
continue
2018-01-21 22:19:39 +00:00
}
2018-01-30 00:48:14 +00:00
newInfo . name = "?"
newInfo . key = key
2018-06-02 13:24:06 +00:00
newInfo . options = "fontname=\"sans serif\" style=dashed color=\"#999999\" fontcolor=\"#999999\""
2018-09-05 00:30:07 +00:00
coordsSplit := coordSlice ( newInfo . key )
if len ( coordsSplit ) != 0 {
portStr := coordsSplit [ len ( coordsSplit ) - 1 ]
portUint , err := strconv . ParseUint ( portStr , 10 , 64 )
if err == nil {
newInfo . port = switchPort ( portUint )
}
}
2018-01-30 00:48:14 +00:00
infos [ key ] = newInfo
2018-01-21 22:19:39 +00:00
}
2018-01-30 00:48:14 +00:00
}
// Now go through and attach parents
for _ , info := range infos {
pSplit := coordSlice ( info . key )
if len ( pSplit ) > 0 {
pSplit = pSplit [ : len ( pSplit ) - 1 ]
2018-01-21 20:58:54 +00:00
}
2018-01-30 00:48:14 +00:00
info . parent = fmt . Sprintf ( "[%v]" , strings . Join ( pSplit , " " ) )
infos [ info . key ] = info
}
// Finally, get a sorted list of keys, which we use to organize the output
var keys [ ] string
for _ , info := range infos {
keys = append ( keys , info . key )
}
2018-06-12 22:50:08 +00:00
// sort
2018-07-21 04:02:25 +00:00
sort . SliceStable ( keys , func ( i , j int ) bool {
2018-01-30 00:48:14 +00:00
return keys [ i ] < keys [ j ]
2018-07-21 04:02:25 +00:00
} )
sort . SliceStable ( keys , func ( i , j int ) bool {
return infos [ keys [ i ] ] . port < infos [ keys [ j ] ] . port
} )
2018-01-30 00:48:14 +00:00
// Now print it all out
var out [ ] byte
put := func ( s string ) {
out = append ( out , [ ] byte ( s ) ... )
}
put ( "digraph {\n" )
// First set the labels
for _ , key := range keys {
info := infos [ key ]
2018-06-01 01:28:09 +00:00
put ( fmt . Sprintf ( "\"%v\" [ label = \"%v\" %v ];\n" , info . key , info . name , info . options ) )
2018-01-21 00:17:15 +00:00
}
2018-01-30 00:48:14 +00:00
// Then print the tree structure
for _ , key := range keys {
info := infos [ key ]
if info . key == info . parent {
continue
} // happens for the root, skip it
2018-07-21 04:02:25 +00:00
port := fmt . Sprint ( info . port )
2018-06-02 13:24:06 +00:00
style := "fontname=\"sans serif\""
if infos [ info . parent ] . name == "?" || infos [ info . key ] . name == "?" {
style = "fontname=\"sans serif\" style=dashed color=\"#999999\" fontcolor=\"#999999\""
}
put ( fmt . Sprintf ( " \"%+v\" -> \"%+v\" [ label = \"%v\" %s ];\n" , info . parent , info . key , port , style ) )
2018-01-21 00:17:15 +00:00
}
2018-01-30 00:48:14 +00:00
put ( "}\n" )
return out
2018-01-21 00:17:15 +00:00
}