Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
*.exe
|
||||||
|
./.idea
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[core]
|
||||||
|
auth_key = "" # required
|
||||||
|
control_url = "https://controlplane.tailscale.com" # or your headscale instance
|
||||||
|
hostname = "" # leave blank to use machine name
|
||||||
|
ephemeral = true
|
||||||
|
accept_routes = true
|
||||||
|
|
||||||
|
[[forward.web]] # you -> others
|
||||||
|
protocol = "tcp"
|
||||||
|
tailscale_port = 8080
|
||||||
|
local_addr = "127.0.0.1:9090"
|
||||||
|
|
||||||
|
[[connect.web]] # others -> you
|
||||||
|
protocol = "tcp"
|
||||||
|
local_port = 9000
|
||||||
|
dst_addr = "any-client-in.ts.net:8080"
|
||||||
|
|
||||||
|
[[connect.minecraft]]
|
||||||
|
protocol = "minecraft"
|
||||||
|
local_port = 25565
|
||||||
|
dst_addr = "any-client-in.ts.net:25566"
|
||||||
|
lan_enable = true
|
||||||
|
lan_motd = "Minecraft via Tailscale"
|
||||||
|
|
||||||
|
[[connect.udp_example]]
|
||||||
|
protocol = "udp"
|
||||||
|
local_port = 24454
|
||||||
|
dst_addr = "any-client-in.ts.net:24454"
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForwardRule struct {
|
||||||
|
Protocol string `toml:"protocol"`
|
||||||
|
TailscalePort int `toml:"tailscale_port"`
|
||||||
|
LocalAddr string `toml:"local_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectRule struct {
|
||||||
|
Protocol string `toml:"protocol"`
|
||||||
|
LocalPort int `toml:"local_port"`
|
||||||
|
LocalAddr string `toml:"local_addr"`
|
||||||
|
DstAddr string `toml:"dst_addr"`
|
||||||
|
LanEnable *bool `toml:"lan_enable"`
|
||||||
|
LanMotd string `toml:"lan_motd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Core struct {
|
||||||
|
AuthKey string `toml:"auth_key"`
|
||||||
|
ControlURL string `toml:"control_url"`
|
||||||
|
Hostname string `toml:"hostname"`
|
||||||
|
Ephemeral bool `toml:"ephemeral"`
|
||||||
|
AcceptRoutes bool `toml:"accept_routes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ConnectRule) LANEnabled() bool {
|
||||||
|
if r.LanEnable != nil {
|
||||||
|
return *r.LanEnable
|
||||||
|
}
|
||||||
|
return r.Protocol == "minecraft"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ConnectRule) LANMotdOr(def string) string {
|
||||||
|
if r.LanMotd != "" {
|
||||||
|
return r.LanMotd
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Core Core `toml:"core"`
|
||||||
|
Forward map[string][]ForwardRule `toml:"forward"`
|
||||||
|
Connect map[string][]ConnectRule `toml:"connect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
hostname = "unknown"
|
||||||
|
}
|
||||||
|
cfg := &Config{
|
||||||
|
Core: Core{
|
||||||
|
Hostname: hostname,
|
||||||
|
Ephemeral: true,
|
||||||
|
AcceptRoutes: true,
|
||||||
|
},
|
||||||
|
Forward: make(map[string][]ForwardRule),
|
||||||
|
Connect: make(map[string][]ConnectRule),
|
||||||
|
}
|
||||||
|
if _, err = os.Stat(path); err != nil {
|
||||||
|
// write a basic config
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err = toml.NewEncoder(buf).Encode(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path, buf.Bytes(), 0644)
|
||||||
|
}
|
||||||
|
_, err = toml.DecodeFile(path, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/tsnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartForwarders(ctx context.Context, srv *tsnet.Server, rules map[string][]ForwardRule) {
|
||||||
|
for tag, rrs := range rules {
|
||||||
|
for _, rule := range rrs {
|
||||||
|
rule := rule
|
||||||
|
tag := tag
|
||||||
|
slog.Debug("starting forwarder",
|
||||||
|
slog.String("tag", tag),
|
||||||
|
slog.String("protocol", rule.Protocol),
|
||||||
|
slog.Int("tailscale_port", rule.TailscalePort),
|
||||||
|
slog.String("local_addr", rule.LocalAddr),
|
||||||
|
)
|
||||||
|
go runForwarder(ctx, srv, rule, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartConnectors(ctx context.Context, srv *tsnet.Server, rules map[string][]ConnectRule) {
|
||||||
|
for tag, rrs := range rules {
|
||||||
|
for _, rule := range rrs {
|
||||||
|
rule := rule
|
||||||
|
tag := tag
|
||||||
|
args := []any{
|
||||||
|
slog.String("tag", tag),
|
||||||
|
slog.String("protocol", rule.Protocol),
|
||||||
|
slog.Int("local_port", rule.LocalPort),
|
||||||
|
slog.String("dst_addr", rule.DstAddr),
|
||||||
|
}
|
||||||
|
if rule.LocalAddr != "" {
|
||||||
|
args = append(args, slog.String("local_addr", rule.LocalAddr))
|
||||||
|
}
|
||||||
|
slog.Debug("starting connector", args...)
|
||||||
|
go runConnector(ctx, srv, rule, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuleLogger(rule any, tag string) *slog.Logger {
|
||||||
|
var args []any
|
||||||
|
switch r := rule.(type) {
|
||||||
|
case ForwardRule:
|
||||||
|
args = []any{
|
||||||
|
slog.String("protocol", r.Protocol),
|
||||||
|
slog.Int("tailscale_port", r.TailscalePort),
|
||||||
|
slog.String("local_addr", r.LocalAddr),
|
||||||
|
}
|
||||||
|
case ConnectRule:
|
||||||
|
args = []any{
|
||||||
|
slog.String("protocol", r.Protocol),
|
||||||
|
slog.Int("local_port", r.LocalPort),
|
||||||
|
slog.String("dst_addr", r.DstAddr),
|
||||||
|
}
|
||||||
|
if r.LocalAddr != "" {
|
||||||
|
args = append(args, slog.String("local_addr", r.LocalAddr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tag != "" {
|
||||||
|
args = append(args, slog.String("tag", tag))
|
||||||
|
}
|
||||||
|
return slog.With(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, tag string) {
|
||||||
|
logger := RuleLogger(rule, tag)
|
||||||
|
|
||||||
|
switch rule.Protocol {
|
||||||
|
case "tcp":
|
||||||
|
runTCPForwarder(ctx, srv, rule, logger)
|
||||||
|
case "udp":
|
||||||
|
runUDPForwarder(ctx, srv, rule, logger)
|
||||||
|
default:
|
||||||
|
logger.Error("unsupported protocol, expected tcp or udp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTCPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, logger *slog.Logger) {
|
||||||
|
ip4, ip6 := srv.TailscaleIPs()
|
||||||
|
ip := ip4
|
||||||
|
if !ip.IsValid() {
|
||||||
|
ip = ip6
|
||||||
|
}
|
||||||
|
ln, err := srv.Listen("tcp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to listen", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort)))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
ln.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("accept error", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go handleTCPForward(ctx, srv, conn, rule, logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTCPForward(ctx context.Context, srv *tsnet.Server, conn net.Conn, rule ForwardRule, logger *slog.Logger) {
|
||||||
|
remoteAddrStr := conn.RemoteAddr().String()
|
||||||
|
clog := logger.With(slog.String("remote", remoteAddrStr))
|
||||||
|
|
||||||
|
lc, err := srv.LocalClient()
|
||||||
|
if err == nil {
|
||||||
|
who, err := lc.WhoIs(ctx, remoteAddrStr)
|
||||||
|
if err == nil {
|
||||||
|
clog = clog.With(slog.String("user", who.UserProfile.LoginName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connType := getConnType(ctx, srv, remoteAddrStr)
|
||||||
|
clog.Info("accepted connection",
|
||||||
|
slog.String("conn_type", connType),
|
||||||
|
slog.String("local_addr", rule.LocalAddr),
|
||||||
|
)
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
localConn, err := net.Dial("tcp", rule.LocalAddr)
|
||||||
|
if err != nil {
|
||||||
|
clog.Error("failed to dial local", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer localConn.Close()
|
||||||
|
|
||||||
|
toTs, toLocal := pipeConns(conn, localConn)
|
||||||
|
clog.Info("connection closed", slog.Int64("ts_rx_bytes", toLocal), slog.Int64("ts_tx_bytes", toTs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConnType(ctx context.Context, srv *tsnet.Server, remoteAddrStr string) string {
|
||||||
|
lc, err := srv.LocalClient()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
status, err := lc.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteHost, _, err := net.SplitHostPort(remoteAddrStr)
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range status.Peer {
|
||||||
|
for _, addr := range peer.TailscaleIPs {
|
||||||
|
if addr.String() == remoteHost {
|
||||||
|
if peer.Relay != "" {
|
||||||
|
return fmt.Sprintf("derp(%s)", peer.Relay)
|
||||||
|
}
|
||||||
|
return "direct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUDPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, logger *slog.Logger) {
|
||||||
|
ip4, ip6 := srv.TailscaleIPs()
|
||||||
|
ip := ip4
|
||||||
|
if !ip.IsValid() {
|
||||||
|
ip = ip6
|
||||||
|
}
|
||||||
|
pc, err := srv.ListenPacket("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to listen", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort)))
|
||||||
|
|
||||||
|
relay := &udpRelay{
|
||||||
|
listenConn: pc,
|
||||||
|
dialAddr: rule.LocalAddr,
|
||||||
|
logger: logger,
|
||||||
|
direction: "local",
|
||||||
|
sessions: make(map[string]*udpSession),
|
||||||
|
}
|
||||||
|
relay.run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpSession struct {
|
||||||
|
conn net.Conn
|
||||||
|
remote net.Addr
|
||||||
|
lastUse time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpRelay struct {
|
||||||
|
listenConn net.PacketConn
|
||||||
|
dialAddr string
|
||||||
|
logger *slog.Logger
|
||||||
|
direction string
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
sessions map[string]*udpSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRelay) run(ctx context.Context) {
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
r.listenConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(2 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
r.cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
buf := make([]byte, 65535)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, from, err := r.listenConn.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.logger.Error("udp read error", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := from.String()
|
||||||
|
r.mu.Lock()
|
||||||
|
sess, exists := r.sessions[key]
|
||||||
|
if !exists {
|
||||||
|
dialed, err := net.Dial("udp", r.dialAddr)
|
||||||
|
if err != nil {
|
||||||
|
r.mu.Unlock()
|
||||||
|
r.logger.Error("failed to dial", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sess = &udpSession{conn: dialed, remote: from, lastUse: time.Now()}
|
||||||
|
r.sessions[key] = sess
|
||||||
|
r.mu.Unlock()
|
||||||
|
|
||||||
|
r.logger.Debug("new udp session", slog.String("remote", key), slog.String("direction", r.direction))
|
||||||
|
go r.readSession(key, sess)
|
||||||
|
} else {
|
||||||
|
sess.lastUse = time.Now()
|
||||||
|
r.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sess.conn.Write(buf[:n]); err != nil {
|
||||||
|
r.logger.Error("failed to write", "error", err)
|
||||||
|
r.removeSession(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRelay) readSession(key string, sess *udpSession) {
|
||||||
|
buf := make([]byte, 65535)
|
||||||
|
for {
|
||||||
|
n, err := sess.conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
r.removeSession(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := r.listenConn.WriteTo(buf[:n], sess.remote); err != nil {
|
||||||
|
r.logger.Error("failed to write back", "error", err)
|
||||||
|
r.removeSession(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRelay) removeSession(key string) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if s, ok := r.sessions[key]; ok {
|
||||||
|
s.conn.Close()
|
||||||
|
delete(r.sessions, key)
|
||||||
|
r.logger.Debug("udp session closed", slog.String("remote", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRelay) cleanup() {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
threshold := time.Now().Add(-5 * time.Minute)
|
||||||
|
for key, s := range r.sessions {
|
||||||
|
if s.lastUse.Before(threshold) {
|
||||||
|
s.conn.Close()
|
||||||
|
delete(r.sessions, key)
|
||||||
|
r.logger.Debug("udp session cleaned up", slog.String("remote", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConnector(ctx context.Context, srv *tsnet.Server, rule ConnectRule, tag string) {
|
||||||
|
logger := RuleLogger(rule, tag)
|
||||||
|
|
||||||
|
switch rule.Protocol {
|
||||||
|
case "tcp", "minecraft":
|
||||||
|
runTCPConnector(ctx, srv, rule, logger)
|
||||||
|
case "udp":
|
||||||
|
runUDPConnector(ctx, srv, rule, logger)
|
||||||
|
default:
|
||||||
|
logger.Error("unsupported protocol, expected tcp or udp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTCPConnector(ctx context.Context, srv *tsnet.Server, rule ConnectRule, logger *slog.Logger) {
|
||||||
|
bindIP := rule.LocalAddr
|
||||||
|
if bindIP == "" {
|
||||||
|
bindIP = "0.0.0.0"
|
||||||
|
}
|
||||||
|
if rule.LANEnabled() && bindIP != "0.0.0.0" {
|
||||||
|
logger.Warn("lan_enable forces local_addr to 0.0.0.0, overriding")
|
||||||
|
bindIP = "0.0.0.0"
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("%s:%d", bindIP, rule.LocalPort)
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to listen locally", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("listening", slog.String("on", addr))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
ln.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("accept error", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go handleTCPConnect(ctx, srv, conn, rule, logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTCPConnect(ctx context.Context, srv *tsnet.Server, conn net.Conn, rule ConnectRule, logger *slog.Logger) {
|
||||||
|
clog := logger.With(slog.String("local_client", conn.RemoteAddr().String()))
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
tsConn, err := srv.Dial(ctx, "tcp", rule.DstAddr)
|
||||||
|
if err != nil {
|
||||||
|
clog.Error("failed to dial tailscale", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tsConn.Close()
|
||||||
|
|
||||||
|
clog.Info("accepted connection", slog.String("dst_addr", rule.DstAddr))
|
||||||
|
toConn, toTs := pipeConns(conn, tsConn)
|
||||||
|
clog.Info("connection closed", slog.Int64("ts_rx_bytes", toTs), slog.Int64("ts_tx_bytes", toConn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUDPConnector(ctx context.Context, srv *tsnet.Server, rule ConnectRule, logger *slog.Logger) {
|
||||||
|
bindIP := rule.LocalAddr
|
||||||
|
if bindIP == "" {
|
||||||
|
bindIP = "0.0.0.0"
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("%s:%d", bindIP, rule.LocalPort)
|
||||||
|
addrUDP, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to resolve local addr", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := net.ListenUDP("udp", addrUDP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to listen locally", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("listening", slog.String("on", addr))
|
||||||
|
|
||||||
|
relay := &udpRelay{
|
||||||
|
listenConn: pc,
|
||||||
|
dialAddr: rule.DstAddr,
|
||||||
|
logger: logger,
|
||||||
|
direction: "tailscale",
|
||||||
|
sessions: make(map[string]*udpSession),
|
||||||
|
}
|
||||||
|
relay.run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeConns(a, b net.Conn) (toA, toB int64) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
n, _ := io.Copy(a, b)
|
||||||
|
toA = n
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
n, _ := io.Copy(b, a)
|
||||||
|
toB = n
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return
|
||||||
|
}
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LanEntry struct {
|
||||||
|
Motd string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
func LanDiscoverService(ctx context.Context, entryList []LanEntry, logger *slog.Logger) {
|
||||||
|
mcastAddrs := []string{
|
||||||
|
"224.0.2.60:4445",
|
||||||
|
"[ff75:230::60]:4445",
|
||||||
|
}
|
||||||
|
|
||||||
|
var fdList []*net.UDPConn
|
||||||
|
for _, addrStr := range mcastAddrs {
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", addrStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("failed to resolve udp address")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fd, err := net.DialUDP("udp", nil, addr)
|
||||||
|
if err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("failed to dial udp server")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fdList = append(fdList, fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fdList) == 0 {
|
||||||
|
// init fail
|
||||||
|
logger.Warn("all multicast binding failed, service discovery is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entryList {
|
||||||
|
// debug log to print entry detail
|
||||||
|
logger.With(
|
||||||
|
slog.Int("port", entry.Port),
|
||||||
|
slog.String("motd", entry.Motd),
|
||||||
|
).Debug("discover service: %s on %d", entry.Motd, entry.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug("shutting down lan discovery service")
|
||||||
|
for _, fd := range fdList {
|
||||||
|
fd.Close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
for _, e := range entryList {
|
||||||
|
msg := fmt.Sprintf("[MOTD]%s[/MOTD][AD]%d[/AD]", e.Motd, e.Port)
|
||||||
|
for _, c := range fdList {
|
||||||
|
_, err := c.Write([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("failed to write to udp server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunLanDiscoverService(ctx context.Context, rules map[string][]ConnectRule, logger *slog.Logger) {
|
||||||
|
var lanEntries []LanEntry
|
||||||
|
for tag, rs := range rules {
|
||||||
|
for _, rule := range rs {
|
||||||
|
if !rule.LANEnabled() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
motd := rule.LANMotdOr(tag)
|
||||||
|
lanEntries = append(lanEntries, LanEntry{
|
||||||
|
Motd: motd,
|
||||||
|
Port: rule.LocalPort,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go LanDiscoverService(ctx, lanEntries, logger)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseLevel(s string) slog.Level {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "debug":
|
||||||
|
return slog.LevelDebug
|
||||||
|
case "warn", "warning":
|
||||||
|
return slog.LevelWarn
|
||||||
|
case "error":
|
||||||
|
return slog.LevelError
|
||||||
|
default:
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(level string, useJsonFormat bool) *slog.Logger {
|
||||||
|
w := os.Stdout
|
||||||
|
var logger *slog.Logger
|
||||||
|
if !useJsonFormat {
|
||||||
|
logger = slog.New(tint.NewHandler(colorable.NewColorable(w), &tint.Options{
|
||||||
|
Level: parseLevel(level),
|
||||||
|
TimeFormat: time.DateTime,
|
||||||
|
//NoColor: !isatty.IsTerminal(w.Fd()),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
logger = slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
|
||||||
|
Level: parseLevel(level),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
return logger
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
fmt2 "fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
|
||||||
|
"tailscale.com/tsnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitTsNet(ctx context.Context, cfg *Core, logger *slog.Logger) (*tsnet.Server, error) {
|
||||||
|
srv := &tsnet.Server{
|
||||||
|
Hostname: "tslink-" + cfg.Hostname,
|
||||||
|
AuthKey: cfg.AuthKey,
|
||||||
|
Ephemeral: cfg.Ephemeral,
|
||||||
|
Logf: func(fmt string, args ...interface{}) {
|
||||||
|
logger.With(slog.String("from", "tsnet")).Debug(fmt2.Sprintf(fmt, args...))
|
||||||
|
},
|
||||||
|
UserLogf: func(fmt string, args ...interface{}) {
|
||||||
|
logger.With(slog.String("from", "tsnet")).Info(fmt2.Sprintf(fmt, args...))
|
||||||
|
},
|
||||||
|
RunWebClient: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ControlURL != "" {
|
||||||
|
srv.ControlURL = cfg.ControlURL
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("starting tsnet server")
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("starting tsnet server failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := srv.Up(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("bring up tsnet server failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range status.TailscaleIPs {
|
||||||
|
logger.With(slog.String("ip", ip.String())).Info("ip got from tsnet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.AcceptRoutes {
|
||||||
|
lc, err := srv.LocalClient()
|
||||||
|
if err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("error from getting local client")
|
||||||
|
} else {
|
||||||
|
_, err = lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||||
|
Prefs: ipn.Prefs{RouteAll: true},
|
||||||
|
RouteAllSet: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.With(slog.String("error", err.Error())).Error("error from editing prefs")
|
||||||
|
} else {
|
||||||
|
logger.Debug("subnet route accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
module tslink
|
||||||
|
|
||||||
|
go 1.26.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.6.0
|
||||||
|
github.com/lmittmann/tint v1.1.3
|
||||||
|
github.com/mattn/go-colorable v0.1.13
|
||||||
|
tailscale.com v1.98.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||||
|
github.com/coder/websocket v1.8.12 // indirect
|
||||||
|
github.com/creachadair/msync v0.7.1 // indirect
|
||||||
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
|
github.com/gaissmai/bart v0.26.1 // indirect
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/btree v1.1.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
|
github.com/huin/goupnp v1.3.0 // indirect
|
||||||
|
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||||
|
github.com/mdlayher/socket v0.5.0 // indirect
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
|
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||||
|
github.com/safchain/ethtool v0.3.0 // indirect
|
||||||
|
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||||
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||||
|
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||||
|
github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/net v0.53.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
golang.org/x/term v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.36.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
|
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"tslink/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
useJsonFormatLogger := flag.Bool("json-format", false, "use json format logger")
|
||||||
|
logLevel := flag.String("level", "info", "log level (DEBUG|INFO|WARN|ERROR)")
|
||||||
|
configPath := flag.String("c", "config.toml", "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
logger := core.NewLogger(*logLevel, *useJsonFormatLogger)
|
||||||
|
|
||||||
|
logger.Info("Starting tslink server", "level", *logLevel)
|
||||||
|
cfg, err := core.LoadConfig(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.With(
|
||||||
|
slog.String("error", err.Error()),
|
||||||
|
).Error("Error loading config")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
logger.Info("initializing tsnet server")
|
||||||
|
srv, err := core.InitTsNet(ctx, &cfg.Core, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.With(
|
||||||
|
slog.String("error", err.Error())).Error("Error initializing tsnet")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.Info("tsnet server initialized")
|
||||||
|
|
||||||
|
core.StartForwarders(ctx, srv, cfg.Forward)
|
||||||
|
core.StartConnectors(ctx, srv, cfg.Connect)
|
||||||
|
|
||||||
|
core.RunLanDiscoverService(ctx, cfg.Connect, logger)
|
||||||
|
|
||||||
|
sigHandler := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigHandler, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sigHandler:
|
||||||
|
logger.Warn("Shutting down")
|
||||||
|
cancel()
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user