Open-Source Licences

Last updated: 5 October 2026

Skimmo’s proxy engine is built on Xray-core, licensed under the Mozilla Public License 2.0, and on the open-source libraries it depends on. The full list, with each component’s licence text, is on the app’s Settings → About → Open-source licences screen. Skimmo modifies two of those components; both changes are published in full below.

Xray-core

Skimmo builds Xray-core from the pinned upstream version (v1.260327.1-0.20260908222543-52a412d9e2f5, the v26.9.9 release commit) with one change. Upstream’s Shadowsocks 2022 support is built on two GPL-3.0 libraries (sagernet/sing and sagernet/sing-shadowsocks). Skimmo’s build runs the Shadowsocks 2022 client on go-shadowsocks2 (Apache-2.0) instead, and leaves out the Shadowsocks 2022 server side, which only those libraries implement, so Skimmo contains no GPL/AGPL code.

The Mozilla Public License is file-level copyleft: modifying an MPL file means the source of that modification must be made available. This is that publication: the patch below, against the upstream version above, is the complete Source Code Form of the modification. Before applying it, the build deletes proxy/shadowsocks_2022/inbound.go, inbound_multi.go, inbound_relay.go and common/singbridge.

--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@
 go 1.27
 
 require (
+	github.com/go-gost/go-shadowsocks2 v0.1.4
 	github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
 	github.com/cloudflare/circl v1.6.5
 	github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
@@ -18,8 +19,6 @@
 	github.com/pires/go-proxyproto v0.15.0
 	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
 	github.com/robfig/cron/v3 v3.0.1
-	github.com/sagernet/sing v0.5.1
-	github.com/sagernet/sing-shadowsocks v0.2.7
 	github.com/stretchr/testify v1.12.1
 	github.com/vishvananda/netlink v1.3.1
 	github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
--- a/infra/conf/shadowsocks.go
+++ b/infra/conf/shadowsocks.go
@@ -3,8 +3,6 @@
 import (
 	"strings"
 
-	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
-	C "github.com/sagernet/sing/common"
 	"github.com/xtls/xray-core/common/errors"
 	"github.com/xtls/xray-core/common/protocol"
 	"github.com/xtls/xray-core/common/serial"
@@ -14,6 +12,15 @@
 	"google.golang.org/protobuf/proto"
 )
 
+// isShadowsocks2022 reports whether the method is one of the Shadowsocks 2022 ciphers. Upstream's
+// implementation pulls in the GPL-3 sagernet/sing modules; this build's client runs on
+// go-shadowsocks2 (Apache-2.0) instead (see proxy/shadowsocks_2022/outbound.go), and has no server.
+func isShadowsocks2022(cipher string) bool {
+	return strings.HasPrefix(strings.ToLower(cipher), "2022-blake3-")
+}
+
+var errShadowsocks2022Server = errors.New("Shadowsocks 2022 servers are not supported in this build")
+
 func cipherFromString(c string) shadowsocks.CipherType {
 	switch strings.ToLower(c) {
 	case "aes-128-gcm", "aead_aes_128_gcm":
@@ -55,8 +62,8 @@
 		v.Users = v.Clients
 	}
 
-	if C.Contains(shadowaead_2022.List, v.Cipher) {
-		return buildShadowsocks2022(v)
+	if isShadowsocks2022(v.Cipher) {
+		return nil, errShadowsocks2022Server
 	}
 
 	config := new(shadowsocks.ServerConfig)
@@ -110,72 +117,6 @@
 	return config, nil
 }
 
-func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
-	if len(v.Users) == 0 {
-		config := new(shadowsocks_2022.ServerConfig)
-		config.Method = v.Cipher
-		config.Key = v.Password
-		config.Network = v.NetworkList.Build()
-		config.Email = v.Email
-		return config, nil
-	}
-
-	if v.Cipher == "" {
-		return nil, errors.New("shadowsocks 2022 (multi-user): missing server method")
-	}
-	if !strings.Contains(v.Cipher, "aes") {
-		return nil, errors.New("shadowsocks 2022 (multi-user): only blake3-aes-*-gcm methods are supported")
-	}
-
-	if v.Users[0].Address == nil {
-		config := new(shadowsocks_2022.MultiUserServerConfig)
-		config.Method = v.Cipher
-		config.Key = v.Password
-		config.Network = v.NetworkList.Build()
-
-		config.Users = make([]*protocol.User, len(v.Users))
-		processUser := func(idx int) error {
-			user := v.Users[idx]
-			if user.Cipher != "" {
-				return errors.New("shadowsocks 2022 (multi-user): users must have empty method")
-			}
-			account := &shadowsocks_2022.Account{
-				Key: user.Password,
-			}
-			config.Users[idx] = &protocol.User{
-				Email:   user.Email,
-				Level:   uint32(user.Level),
-				Account: serial.ToTypedMessage(account),
-			}
-			return nil
-		}
-		if err := task.ParallelForN(len(v.Users), processUser); err != nil {
-			return nil, err
-		}
-		return config, nil
-	}
-
-	config := new(shadowsocks_2022.RelayServerConfig)
-	config.Method = v.Cipher
-	config.Key = v.Password
-	config.Network = v.NetworkList.Build()
-	for _, user := range v.Users {
-		if user.Cipher != "" {
-			return nil, errors.New("shadowsocks 2022 (relay): users must have empty method")
-		}
-		if user.Address == nil {
-			return nil, errors.New("shadowsocks 2022 (relay): all users must have relay address")
-		}
-		config.Destinations = append(config.Destinations, &shadowsocks_2022.RelayDestination{
-			Key:     user.Password,
-			Email:   user.Email,
-			Address: user.Address.Build(),
-			Port:    uint32(user.Port),
-		})
-	}
-	return config, nil
-}
-
 type ShadowsocksServerTarget struct {
 	Address  *Address `json:"address"`
 	Port     uint16   `json:"port"`
@@ -214,31 +155,31 @@
 		return nil, errors.New(`Shadowsocks settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Shadowsocks outbounds and routing balancer instead`)
 	}
 
-	if len(v.Servers) == 1 {
+	if len(v.Servers) == 1 && isShadowsocks2022(v.Servers[0].Cipher) {
 		server := v.Servers[0]
-		if C.Contains(shadowaead_2022.List, server.Cipher) {
-			if server.Address == nil {
-				return nil, errors.New("Shadowsocks server address is not set.")
-			}
-			if server.Port == 0 {
-				return nil, errors.New("Invalid Shadowsocks port.")
-			}
-			if server.Password == "" {
-				return nil, errors.New("Shadowsocks password is not specified.")
-			}
-
-			config := new(shadowsocks_2022.ClientConfig)
-			config.Address = server.Address.Build()
-			config.Port = uint32(server.Port)
-			config.Method = server.Cipher
-			config.Key = server.Password
-			return config, nil
+		if !shadowsocks_2022.IsSupportedMethod(server.Cipher) {
+			return nil, errors.New("Shadowsocks 2022 method ", server.Cipher, " is not supported in this build")
 		}
+		if server.Address == nil {
+			return nil, errors.New("Shadowsocks server address is not set.")
+		}
+		if server.Port == 0 {
+			return nil, errors.New("Invalid Shadowsocks port.")
+		}
+		if server.Password == "" {
+			return nil, errors.New("Shadowsocks password is not specified.")
+		}
+		config := new(shadowsocks_2022.ClientConfig)
+		config.Address = server.Address.Build()
+		config.Port = uint32(server.Port)
+		config.Method = server.Cipher
+		config.Key = server.Password
+		return config, nil
 	}
 
 	config := new(shadowsocks.ClientConfig)
 	for _, server := range v.Servers {
-		if C.Contains(shadowaead_2022.List, server.Cipher) {
+		if isShadowsocks2022(server.Cipher) {
 			return nil, errors.New("Shadowsocks 2022 accept no multi servers")
 		}
 		if server.Address == nil {
--- a/proxy/shadowsocks_2022/outbound.go
+++ b/proxy/shadowsocks_2022/outbound.go
@@ -1,22 +1,30 @@
 package shadowsocks_2022
 
+// Skimmo change (2026-09-27): this outbound is rewritten on top of
+// github.com/go-gost/go-shadowsocks2/shadowaead_2022 (Apache-2.0) instead of
+// github.com/sagernet/sing-shadowsocks (GPL-3.0), so the build carries no GPL code. Client only;
+// the server-side handlers (inbound*.go) are not part of this build. The TCP/UDP copy loops follow
+// Xray's own proxy/shadowsocks client.
+
 import (
 	"context"
-	"time"
+	"crypto/rand"
+	"encoding/binary"
+	"sync"
+	"sync/atomic"
 
-	shadowsocks "github.com/sagernet/sing-shadowsocks"
-	"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
-	C "github.com/sagernet/sing/common"
-	B "github.com/sagernet/sing/common/buf"
-	"github.com/sagernet/sing/common/bufio"
-	N "github.com/sagernet/sing/common/network"
+	"github.com/go-gost/go-shadowsocks2/core"
+	shadowaead_2022 "github.com/go-gost/go-shadowsocks2/shadowaead_2022"
+	"github.com/go-gost/go-shadowsocks2/socks"
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common/buf"
 	"github.com/xtls/xray-core/common/errors"
 	"github.com/xtls/xray-core/common/net"
 	"github.com/xtls/xray-core/common/session"
 	"github.com/xtls/xray-core/common/signal"
-	"github.com/xtls/xray-core/common/singbridge"
+	"github.com/xtls/xray-core/common/task"
+	xcore "github.com/xtls/xray-core/core"
+	"github.com/xtls/xray-core/features/policy"
 	"github.com/xtls/xray-core/transport"
 	"github.com/xtls/xray-core/transport/internet"
 )
@@ -27,43 +35,50 @@
 	}))
 }
 
+// Methods this build can run. 2022-blake3-chacha20-poly1305 is left out: go-shadowsocks2 does not
+// implement it (its UDP form is a different packet layout, XChaCha20 with no separate header).
+var methods = map[string]core.MetaCipher{
+	"2022-blake3-aes-128-gcm": {Name: "2022-blake3-aes-128-gcm", KeySize: 16, SaltSize: 16, NonceSize: 12, TagSize: 16},
+	"2022-blake3-aes-256-gcm": {Name: "2022-blake3-aes-256-gcm", KeySize: 32, SaltSize: 32, NonceSize: 12, TagSize: 16},
+}
+
+// IsSupportedMethod reports whether this build's Shadowsocks 2022 client can run method.
+func IsSupportedMethod(method string) bool {
+	_, ok := methods[method]
+	return ok
+}
+
 type Outbound struct {
-	ctx    context.Context
-	server net.Destination
-	method shadowsocks.Method
+	server        net.Destination
+	cipher        core.ShadowCipher
+	policyManager policy.Manager
 }
 
 func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
-	o := &Outbound{
-		ctx: ctx,
+	meta, ok := methods[config.Method]
+	if !ok {
+		return nil, errors.New("unsupported Shadowsocks 2022 method ", config.Method)
+	}
+	if config.Key == "" {
+		return nil, errors.New("missing psk")
+	}
+	// The key is the base64 PSK, or "iPSK:uPSK" for a relay (extensible identity headers).
+	cipher, err := shadowaead_2022.AESGCM(meta, config.Key)
+	if err != nil {
+		return nil, errors.New("create method").Base(err)
+	}
+	v := xcore.MustFromContext(ctx)
+	return &Outbound{
 		server: net.Destination{
 			Address: config.Address.AsAddress(),
 			Port:    net.Port(config.Port),
-			Network: net.Network_TCP,
 		},
-	}
-	if C.Contains(shadowaead_2022.List, config.Method) {
-		if config.Key == "" {
-			return nil, errors.New("missing psk")
-		}
-		method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil)
-		if err != nil {
-			return nil, errors.New("create method").Base(err)
-		}
-		o.method = method
-	} else {
-		return nil, errors.New("unknown method ", config.Method)
-	}
-	return o, nil
+		cipher:        cipher,
+		policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
+	}, nil
 }
 
 func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
-	var inboundConn net.Conn
-	inbound := session.InboundFromContext(ctx)
-	if inbound != nil {
-		inboundConn = inbound.Conn
-	}
-
 	outbounds := session.OutboundsFromContext(ctx)
 	ob := outbounds[len(outbounds)-1]
 	if !ob.Target.IsValid() {
@@ -72,76 +87,197 @@
 	ob.Name = "shadowsocks-2022"
 	ob.CanSpliceCopy = 3
 	destination := ob.Target
-	network := destination.Network
 
-	errors.LogInfo(ctx, "tunneling request to ", destination, " via ", o.server.NetAddr())
-
 	serverDestination := o.server
-	serverDestination.Network = network
-	connection, err := dialer.Dial(ctx, serverDestination)
+	serverDestination.Network = destination.Network
+	conn, err := dialer.Dial(ctx, serverDestination)
 	if err != nil {
 		return errors.New("failed to connect to server").Base(err)
 	}
-	defer connection.Close()
+	defer conn.Close()
+	errors.LogInfo(ctx, "tunneling request to ", destination, " via ", serverDestination)
 
+	var newCtx context.Context
+	var newCancel context.CancelFunc
 	if session.TimeoutOnlyFromContext(ctx) {
-		ctx, _ = context.WithCancel(context.Background())
+		newCtx, newCancel = context.WithCancel(context.Background())
 	}
+	sessionPolicy := o.policyManager.ForLevel(0)
+	ctx, cancel := context.WithCancel(ctx)
+	timer := signal.CancelAfterInactivity(ctx, func() {
+		cancel()
+		if newCancel != nil {
+			newCancel()
+		}
+	}, sessionPolicy.Timeouts.ConnectionIdle)
+	if newCtx != nil {
+		ctx = newCtx
+	}
 
-	if network == net.Network_TCP {
-		serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
-		var handshake bool
-		if timeoutReader, isTimeoutReader := link.Reader.(buf.TimeoutReader); isTimeoutReader {
-			mb, err := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 100)
-			if err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
-				return errors.New("read payload").Base(err)
-			}
-			payload := B.New()
-			for {
-				payload.Reset()
-				nb, n := buf.SplitBytes(mb, payload.FreeBytes())
-				if n > 0 {
-					payload.Truncate(n)
-					_, err = serverConn.Write(payload.Bytes())
-					if err != nil {
-						payload.Release()
-						return errors.New("write payload").Base(err)
-					}
-					handshake = true
-				}
-				if nb.IsEmpty() {
-					break
-				}
-				mb = nb
-			}
-			payload.Release()
+	var requestDone, responseDone func() error
+	if destination.Network == net.Network_TCP {
+		target := socks.ParseAddr(destination.NetAddr())
+		if target == nil {
+			return errors.New("invalid target ", destination)
 		}
-		if !handshake {
-			_, err = serverConn.Write(nil)
-			if err != nil {
-				return errors.New("client handshake").Base(err)
-			}
+		client := core.NewTCPClient(core.ClientConfig{Cipher: o.cipher})
+		ssConn, err := client.WrapConn(conn, target)
+		if err != nil {
+			return errors.New("client handshake").Base(err)
 		}
-		return singbridge.CopyConn(ctx, inboundConn, link, serverConn)
+		requestDone = func() error {
+			defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
+			return buf.Copy(link.Reader, buf.NewWriter(ssConn), buf.UpdateActivity(timer))
+		}
+		responseDone = func() error {
+			defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
+			return buf.Copy(buf.NewReader(ssConn), link.Writer, buf.UpdateActivity(timer))
+		}
 	} else {
-		var packetConn N.PacketConn
-		if pc, isPacketConn := inboundConn.(N.PacketConn); isPacketConn {
-			packetConn = pc
-		} else if nc, isNetPacket := inboundConn.(net.PacketConn); isNetPacket {
-			packetConn = bufio.NewPacketConn(nc)
-		} else {
-			packetConn = &singbridge.PacketConnWrapper{
-				Reader: link.Reader,
-				Writer: link.Writer,
-				Conn:   inboundConn,
-				Dest:   destination,
-				T: signal.CancelAfterInactivity(ctx, func() {
-					common.Interrupt(link.Reader)
-				}, 300*time.Second),
-			}
+		udp := &udpSession{cipher: o.cipher, conn: conn, target: destination}
+		if err := udp.init(); err != nil {
+			return err
 		}
+		requestDone = func() error {
+			defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
+			return buf.Copy(link.Reader, udp, buf.UpdateActivity(timer))
+		}
+		responseDone = func() error {
+			defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
+			return buf.Copy(udp, link.Writer, buf.UpdateActivity(timer))
+		}
+	}
 
-		serverConn := o.method.DialPacketConn(connection)
-		return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
+	responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
+	if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
+		return errors.New("connection ends").Base(err)
 	}
+	return nil
 }
+
+// udpSession is one SIP022 UDP session: a random session ID and a packet counter, on one socket to
+// the server. Each packet carries its own target, so a flow that talks to several destinations works;
+// replies are matched to this session by their client session ID and keep the source address the
+// server reports, except that a reply to a domain target is tagged with that domain (see
+// replySource).
+type udpSession struct {
+	cipher    core.ShadowCipher
+	conn      net.Conn
+	target    net.Destination
+	sessionID uint64
+	packetID  atomic.Uint64
+
+	mu        sync.Mutex
+	sentIP    map[string]bool              // IP targets written to, by "ip:port"
+	sentByDom map[net.Port]net.Destination // last domain target written to, by port
+}
+
+func (s *udpSession) init() error {
+	var b [8]byte
+	if _, err := rand.Read(b[:]); err != nil {
+		return err
+	}
+	s.sessionID = binary.BigEndian.Uint64(b[:])
+	return nil
+}
+
+// WriteMultiBuffer implements buf.Writer: encrypt and send each packet.
+func (s *udpSession) WriteMultiBuffer(mb buf.MultiBuffer) error {
+	defer buf.ReleaseMulti(mb)
+	eih := len(s.cipher.Keys()) > 1
+	for _, b := range mb {
+		if b == nil {
+			continue
+		}
+		dest := s.target
+		if b.UDP != nil {
+			dest = *b.UDP
+		}
+		target := socks.ParseAddr(dest.NetAddr())
+		if target == nil {
+			return errors.New("invalid UDP target ", dest)
+		}
+		s.remember(dest)
+		packet, err := shadowaead_2022.PackUDP(s.cipher, eih, s.cipher.FirstKey(), s.cipher.Key(),
+			b.Bytes(), target, s.sessionID, s.packetID.Add(1)-1, 0)
+		if err != nil {
+			return errors.New("encrypt UDP packet").Base(err)
+		}
+		if _, err := s.conn.Write(packet); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// ReadMultiBuffer implements buf.Reader: receive, decrypt, and tag each payload with its source.
+func (s *udpSession) ReadMultiBuffer() (buf.MultiBuffer, error) {
+	packet := make([]byte, 65535)
+	for {
+		n, err := s.conn.Read(packet)
+		if err != nil {
+			return nil, err
+		}
+		_, header, payload, _, err := shadowaead_2022.UnpackUDP(s.cipher, nil, packet[:n])
+		if err != nil {
+			errors.LogDebugInner(context.Background(), err, "drop undecryptable UDP packet")
+			continue
+		}
+		if header.Type != shadowaead_2022.HeaderTypeServerStream || header.ClientSessionID != s.sessionID {
+			continue // not a reply to this session
+		}
+		source, err := net.ParseDestination("udp:" + header.Address.String())
+		if err != nil {
+			source = s.target
+		}
+		source = s.replySource(source)
+		b := buf.New()
+		if len(payload) > buf.Size {
+			b.Release()
+			b = buf.NewWithSize(int32(len(payload)))
+		}
+		if _, err := b.Write(payload); err != nil {
+			b.Release()
+			return nil, err
+		}
+		b.UDP = &source
+		return buf.MultiBuffer{b}, nil
+	}
+}
+
+func (s *udpSession) remember(dest net.Destination) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if dest.Address.Family().IsDomain() {
+		if s.sentByDom == nil {
+			s.sentByDom = make(map[net.Port]net.Destination)
+		}
+		s.sentByDom[dest.Port] = dest
+		return
+	}
+	if s.sentIP == nil {
+		s.sentIP = make(map[string]bool)
+	}
+	s.sentIP[dest.NetAddr()] = true
+}
+
+// replySource maps a reply's source back to the domain the packet was sent to. The server resolves a
+// domain target and reports the IP it got a reply from, but the flow asked for the domain (e.g. a
+// FakeDNS address sniffed back to one), and Xray only rewrites replies from the requested address
+// back to the address the app used. Without this, a UDP reply to a domain target comes back from an
+// address the app never sent to and is dropped. Like freedom's UDPOverride: a reply from an IP we
+// never sent to, on a port we sent a domain packet to, is from that domain.
+func (s *udpSession) replySource(source net.Destination) net.Destination {
+	if source.Address.Family().IsDomain() {
+		return source
+	}
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.sentIP[source.NetAddr()] {
+		return source
+	}
+	if d, ok := s.sentByDom[source.Port]; ok {
+		return d
+	}
+	return source
+}

go-shadowsocks2

go-shadowsocks2 v0.1.4 (Apache-2.0) derives the Shadowsocks 2022 UDP session key from the session ID in little-endian byte order, where the specification and other implementations use big-endian, so its UDP traffic could not be decrypted by other servers. Skimmo fixes the byte order:

--- a/shadowaead_2022/packet.go
+++ b/shadowaead_2022/packet.go
@@ -223,7 +223,10 @@
 // Format: AES(separate_header) + AEAD(body)
 func PackUDP(c core.ShadowCipher, eih bool, separateHeaderKey, bodyKey []byte, payload []byte, target socks.Addr, sessionID, packetID, clientSessionID uint64) ([]byte, error) {
 	salt := make([]byte, 8)
-	binary.LittleEndian.PutUint64(salt, sessionID)
+	// Skimmo change (2026-09-27): the session ID is serialised big-endian here, as in the separate
+	// header and in SIP022; upstream v0.1.4 used little-endian, so UDP failed to authenticate against
+	// every other SS2022 implementation (sing-shadowsocks / Xray, shadowsocks-rust).
+	binary.BigEndian.PutUint64(salt, sessionID)
 
 	// Derive session subkey from salt for AEAD
 	aead, err := c.Encrypter(bodyKey, salt)
@@ -364,7 +367,10 @@
 
 func decryptPayload(ciph core.ShadowCipher, decryptKey []byte, separateHeader *SeparateHeader, separateHeaderEncoding []byte, encryptedBody []byte) (*UDPHeader, []byte, error) {
 	salt := make([]byte, 8)
-	binary.LittleEndian.PutUint64(salt, separateHeader.SessionID)
+	// Skimmo change (2026-09-27): the session ID is serialised big-endian here, as in the separate
+	// header and in SIP022; upstream v0.1.4 used little-endian, so UDP failed to authenticate against
+	// every other SS2022 implementation (sing-shadowsocks / Xray, shadowsocks-rust).
+	binary.BigEndian.PutUint64(salt, separateHeader.SessionID)
 
 	// Derive session subkey from salt for AEAD
 	aead, err := ciph.Decrypter(decryptKey, salt)

Routing data bundled with the app

The “Bypass China” and “Blacklist” modes use two public databases. Skimmo ships a copy of each in the app and downloads newer releases of the same files; both are used unmodified.

The app’s licences screen also lists the Go standard library and runtime (BSD-3-Clause, © The Go Authors), which is compiled into the engine.

Typeface

The app’s text and the skimmo wordmark use Outfit, © 2021 The Outfit Project Authors, under the SIL Open Font License 1.1; the full licence is on the app’s licences screen.